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

/// Translate SourceKit placeholder syntax — `<#foo#>` — in `input` to LSP
/// placeholder syntax: `${n:foo}`.
///
/// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an
/// empty string, to prevent the client from inserting special placeholder
/// characters as if they were literal text.
@_spi(Testing)
public func rewriteSourceKitPlaceholders(in input: String, clientSupportsSnippets: Bool) -> String {
  var result = ""
  var nextPlaceholderNumber = 1
  // Current stack of nested placeholders, most nested last. Each element needs
  // to be rendered inside the element before it.
  var placeholders: [(number: Int, contents: String)] = []
  let tokens = tokenize(input)
  for token in tokens {
    switch token {
    case let .text(text):
      if placeholders.isEmpty {
        result += text
      } else {
        placeholders.latest.contents += text
      }

    case .escapeInsidePlaceholder(let character):
      if placeholders.isEmpty {
        result.append(character)
      } else {
        // A closing brace is only escaped _inside_ a placeholder; otherwise the client would include the backslashes
        // literally.
        placeholders.latest.contents += [#"\"#, character]
      }

    case .placeholderOpen:
      placeholders.append((number: nextPlaceholderNumber, contents: ""))
      nextPlaceholderNumber += 1

    case .placeholderClose:
      guard let (number, placeholderBody) = placeholders.popLast() else {
        logger.fault("Invalid placeholder in \(input)")
        return input
      }
      guard let displayName = nameForSnippet(placeholderBody) else {
        logger.fault("Failed to decode placeholder \(placeholderBody) in \(input)")
        return input
      }
      let placeholder =
        clientSupportsSnippets
        ? formatLSPPlaceholder(displayName, number: number)
        : ""
      if placeholders.isEmpty {
        result += placeholder
      } else {
        placeholders.latest.contents += placeholder
      }
    }
  }

  return result
}

/// Scan `input` to identify special elements within: curly braces, which may
/// need to be escaped; and SourceKit placeholder open/close delimiters.
private func tokenize(_ input: String) -> [SnippetToken] {
  var index = input.startIndex
  var isAtEnd: Bool { index == input.endIndex }
  func match(_ char: Character) -> Bool {
    if isAtEnd || input[index] != char {
      return false
    } else {
      input.formIndex(after: &index)
      return true
    }
  }
  func next() -> Character? {
    guard !isAtEnd else { return nil }
    defer { input.formIndex(after: &index) }
    return input[index]
  }

  var tokens: [SnippetToken] = []
  var text = ""
  while let char = next() {
    switch char {
    case "<":
      if match("#") {
        tokens.append(.text(text))
        text.removeAll()
        tokens.append(.placeholderOpen)
      } else {
        text.append(char)
      }

    case "#":
      if match(">") {
        tokens.append(.text(text))
        text.removeAll()
        tokens.append(.placeholderClose)
      } else {
        text.append(char)
      }

    case "$", "}", "\\":
      tokens.append(.text(text))
      text.removeAll()
      tokens.append(.escapeInsidePlaceholder(char))

    case let c:
      text.append(c)
    }
  }

  tokens.append(.text(text))

  return tokens
}

/// A syntactical element inside a SourceKit snippet.
private enum SnippetToken {
  /// A placeholder delimiter.
  case placeholderOpen, placeholderClose
  /// '$', '}' or '\', which need to be escaped when used inside a placeholder.
  case escapeInsidePlaceholder(Character)
  /// Any other consecutive run of characters from the input, which needs no
  /// special treatment.
  case text(String)
}

/// Given the interior text of a SourceKit placeholder, extract a display name
/// suitable for a LSP snippet.
private func nameForSnippet(_ body: String) -> String? {
  var text = rewrappedAsPlaceholder(body)
  return text.withSyntaxText {
    guard let data = RawEditorPlaceholderData(syntaxText: $0) else {
      return nil
    }
    return String(syntaxText: data.typeForExpansionText ?? data.displayText)
  }
}

private let placeholderStart = "<#"
private let placeholderEnd = "#>"
private func rewrappedAsPlaceholder(_ body: String) -> String {
  return placeholderStart + body + placeholderEnd
}

/// Wrap `body` in LSP snippet placeholder syntax, using `number` as the
/// placeholder's index in the snippet.
private func formatLSPPlaceholder(_ body: String, number: Int) -> String {
  "${\(number):\(body)}"
}

private extension Array {
  /// Mutable access to the final element of an array.
  ///
  /// - precondition: The array must not be empty.
  var latest: Element {
    get { self.last! }
    _modify {
      let index = self.index(before: self.endIndex)
      yield &self[index]
    }
  }
}
