/* sbt -- Simple Build Tool
 * Copyright 2011  Mark Harrah
 */
package sbt.complete

import Parser._
import java.io.File
import java.net.URI
import java.lang.Character.{ getType, MATH_SYMBOL, OTHER_SYMBOL, DASH_PUNCTUATION, OTHER_PUNCTUATION, MODIFIER_SYMBOL, CURRENCY_SYMBOL }

/** Provides standard implementations of commonly useful [[Parser]]s. */
trait Parsers {
  /** Matches the end of input, providing no useful result on success. */
  lazy val EOF = not(any)

  /** Parses any single character and provides that character as the result. */
  lazy val any: Parser[Char] = charClass(_ => true, "any character")

  /** Set that contains each digit in a String representation.*/
  lazy val DigitSet = Set("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")

  /** Parses any single digit and provides that digit as a Char as the result.*/
  lazy val Digit = charClass(_.isDigit, "digit") examples DigitSet

  /** Set containing Chars for hexadecimal digits 0-9 and A-F (but not a-f). */
  lazy val HexDigitSet = Set('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')

  /** Parses a single hexadecimal digit (0-9, a-f, A-F). */
  lazy val HexDigit = charClass(c => HexDigitSet(c.toUpper), "hex digit") examples HexDigitSet.map(_.toString)

  /** Parses a single letter, according to Char.isLetter, into a Char. */
  lazy val Letter = charClass(_.isLetter, "letter")

  /** Parses the first Char in an sbt identifier, which must be a [[Letter]].*/
  def IDStart = Letter

  /** Parses an identifier Char other than the first character.  This includes letters, digits, dash `-`, and underscore `_`.*/
  lazy val IDChar = charClass(isIDChar, "ID character")

  /** Parses an identifier String, which must start with [[IDStart]] and contain zero or more [[IDChar]]s after that. */
  lazy val ID = identifier(IDStart, IDChar)

  /** Parses a single operator Char, as allowed by [[isOpChar]]. */
  lazy val OpChar = charClass(isOpChar, "symbol")

  /** Parses a non-empty operator String, which consists only of characters allowed by [[OpChar]]. */
  lazy val Op = OpChar.+.string

  /** Parses either an operator String defined by [[Op]] or a non-symbolic identifier defined by [[ID]]. */
  lazy val OpOrID = ID | Op

  /** Parses a single, non-symbolic Scala identifier Char.  Valid characters are letters, digits, and the underscore character `_`. */
  lazy val ScalaIDChar = charClass(isScalaIDChar, "Scala identifier character")

  /** Parses a non-symbolic Scala-like identifier.  The identifier must start with [[IDStart]] and contain zero or more [[ScalaIDChar]]s after that.*/
  lazy val ScalaID = identifier(IDStart, ScalaIDChar)

  /** Parses a String that starts with `start` and is followed by zero or more characters parsed by `rep`.*/
  def identifier(start: Parser[Char], rep: Parser[Char]): Parser[String] =
    start ~ rep.* map { case x ~ xs => (x +: xs).mkString }

  def opOrIDSpaced(s: String): Parser[Char] =
    if (DefaultParsers.matches(ID, s))
      OpChar | SpaceClass
    else if (DefaultParsers.matches(Op, s))
      IDChar | SpaceClass
    else
      any

  /** Returns true if `c` an operator character. */
  def isOpChar(c: Char) = !isDelimiter(c) && isOpType(getType(c))
  def isOpType(cat: Int) = cat match { case MATH_SYMBOL | OTHER_SYMBOL | DASH_PUNCTUATION | OTHER_PUNCTUATION | MODIFIER_SYMBOL | CURRENCY_SYMBOL => true; case _ => false }
  /** Returns true if `c` is a dash `-`, a letter, digit, or an underscore `_`. */
  def isIDChar(c: Char) = isScalaIDChar(c) || c == '-'

  /** Returns true if `c` is a letter, digit, or an underscore `_`. */
  def isScalaIDChar(c: Char) = c.isLetterOrDigit || c == '_'

  def isDelimiter(c: Char) = c match { case '`' | '\'' | '\"' | /*';' | */ ',' | '.' => true; case _ => false }

  /** Matches a single character that is not a whitespace character. */
  lazy val NotSpaceClass = charClass(!_.isWhitespace, "non-whitespace character")

  /** Matches a single whitespace character, as determined by Char.isWhitespace.*/
  lazy val SpaceClass = charClass(_.isWhitespace, "whitespace character")

  /** Matches a non-empty String consisting of non-whitespace characters. */
  lazy val NotSpace = NotSpaceClass.+.string

  /** Matches a possibly empty String consisting of non-whitespace characters. */
  lazy val OptNotSpace = NotSpaceClass.*.string

  /**
   * Matches a non-empty String consisting of whitespace characters.
   * The suggested tab completion is a single, constant space character.
   */
  lazy val Space = SpaceClass.+.examples(" ")

  /**
   * Matches a possibly empty String consisting of whitespace characters.
   * The suggested tab completion is a single, constant space character.
   */
  lazy val OptSpace = SpaceClass.*.examples(" ")

  /** Parses a non-empty String that contains only valid URI characters, as defined by [[URIChar]].*/
  lazy val URIClass = URIChar.+.string !!! "Invalid URI"

  /** Triple-quotes, as used for verbatim quoting.*/
  lazy val VerbatimDQuotes = "\"\"\""

  /** Double quote character. */
  lazy val DQuoteChar = '\"'

  /** Backslash character. */
  lazy val BackslashChar = '\\'

  /** Matches a single double quote. */
  lazy val DQuoteClass = charClass(_ == DQuoteChar, "double-quote character")

  /** Matches any character except a double quote or whitespace. */
  lazy val NotDQuoteSpaceClass =
    charClass({ c: Char => (c != DQuoteChar) && !c.isWhitespace }, "non-double-quote-space character")

  /** Matches any character except a double quote or backslash. */
  lazy val NotDQuoteBackslashClass =
    charClass({ c: Char => (c != DQuoteChar) && (c != BackslashChar) }, "non-double-quote-backslash character")

  /** Matches a single character that is valid somewhere in a URI. */
  lazy val URIChar = charClass(alphanum) | chars("_-!.~'()*,;:$&+=?/[]@%#")

  /** Returns true if `c` is an ASCII letter or digit. */
  def alphanum(c: Char) = ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')

  /**
   * @param base the directory used for completion proposals (when the user presses the TAB key). Only paths under this
   *             directory will be proposed.
   * @return the file that was parsed from the input string. The returned path may or may not exist.
   */
  def fileParser(base: File): Parser[File] =
    OptSpace ~> StringBasic
      .examples(new FileExamples(base))
      .map(new File(_))

  /** Parses a port number.  Currently, this accepts any integer and presents a tab completion suggestion of `<port>`. */
  lazy val Port = token(IntBasic, "<port>")

  /** Parses a signed integer. */
  lazy val IntBasic = mapOrFail('-'.? ~ Digit.+)(Function.tupled(toInt))

  /** Parses an unsigned integer. */
  lazy val NatBasic = mapOrFail(Digit.+)(_.mkString.toInt)

  private[this] def toInt(neg: Option[Char], digits: Seq[Char]): Int =
    (neg.toSeq ++ digits).mkString.toInt

  /** Parses the lower-case values `true` and `false` into their respesct Boolean values.  */
  lazy val Bool = ("true" ^^^ true) | ("false" ^^^ false)

  /**
   * Parses a potentially quoted String value.  The value may be verbatim quoted ([[StringVerbatim]]),
   * quoted with interpreted escapes ([[StringEscapable]]), or unquoted ([[NotQuoted]]).
   */
  lazy val StringBasic = StringVerbatim | StringEscapable | NotQuoted

  /**
   * Parses a verbatim quoted String value, discarding the quotes in the result.  This kind of quoted text starts with triple quotes `"""`
   * and ends at the next triple quotes and may contain any character in between.
   */
  lazy val StringVerbatim: Parser[String] = VerbatimDQuotes ~>
    any.+.string.filter(!_.contains(VerbatimDQuotes), _ => "Invalid verbatim string") <~
    VerbatimDQuotes

  /**
   * Parses a string value, interpreting escapes and discarding the surrounding quotes in the result.
   * See [[EscapeSequence]] for supported escapes.
   */
  lazy val StringEscapable: Parser[String] =
    (DQuoteChar ~> (NotDQuoteBackslashClass | EscapeSequence).+.string <~ DQuoteChar |
      (DQuoteChar ~ DQuoteChar) ^^^ "")

  /**
   * Parses a single escape sequence into the represented Char.
   * Escapes start with a backslash and are followed by `u` for a [[UnicodeEscape]] or by `b`, `t`, `n`, `f`, `r`, `"`, `'`, `\` for standard escapes.
   */
  lazy val EscapeSequence: Parser[Char] =
    BackslashChar ~> ('b' ^^^ '\b' | 't' ^^^ '\t' | 'n' ^^^ '\n' | 'f' ^^^ '\f' | 'r' ^^^ '\r' |
      '\"' ^^^ '\"' | '\'' ^^^ '\'' | '\\' ^^^ '\\' | UnicodeEscape)

  /**
   * Parses a single unicode escape sequence into the represented Char.
   * A unicode escape begins with a backslash, followed by a `u` and 4 hexadecimal digits representing the unicode value.
   */
  lazy val UnicodeEscape: Parser[Char] =
    ("u" ~> repeat(HexDigit, 4, 4)) map { seq => Integer.parseInt(seq.mkString, 16).toChar }

  /** Parses an unquoted, non-empty String value that cannot start with a double quote and cannot contain whitespace.*/
  lazy val NotQuoted = (NotDQuoteSpaceClass ~ OptNotSpace) map { case (c, s) => c.toString + s }

  /**
   * Applies `rep` zero or more times, separated by `sep`.
   * The result is the (possibly empty) sequence of results from the multiple `rep` applications.  The `sep` results are discarded.
   */
  def repsep[T](rep: Parser[T], sep: Parser[_]): Parser[Seq[T]] =
    rep1sep(rep, sep) ?? Nil

  /**
   * Applies `rep` one or more times, separated by `sep`.
   * The result is the non-empty sequence of results from the multiple `rep` applications.  The `sep` results are discarded.
   */
  def rep1sep[T](rep: Parser[T], sep: Parser[_]): Parser[Seq[T]] =
    (rep ~ (sep ~> rep).*).map { case (x ~ xs) => x +: xs }

  /** Wraps the result of `p` in `Some`.*/
  def some[T](p: Parser[T]): Parser[Option[T]] = p map { v => Some(v) }

  /**
   * Applies `f` to the result of `p`, transforming any exception when evaluating
   * `f` into a parse failure with the exception `toString` as the message.
   */
  def mapOrFail[S, T](p: Parser[S])(f: S => T): Parser[T] =
    p flatMap { s => try { success(f(s)) } catch { case e: Exception => failure(e.toString) } }

  /**
   * Parses a space-delimited, possibly empty sequence of arguments.
   * The arguments may use quotes and escapes according to [[StringBasic]].
   */
  def spaceDelimited(display: String): Parser[Seq[String]] = (token(Space) ~> token(StringBasic, display)).* <~ SpaceClass.*

  /** Applies `p` and uses `true` as the result if it succeeds and turns failure into a result of `false`. */
  def flag[T](p: Parser[T]): Parser[Boolean] = (p ^^^ true) ?? false

  /**
   * Defines a sequence parser where the parser used for each part depends on the previously parsed values.
   * `p` is applied to the (possibly empty) sequence of already parsed values to obtain the next parser to use.
   * The parsers obtained in this way are separated by `sep`, whose result is discarded and only the sequence
   * of values from the parsers returned by `p` is used for the result.
   */
  def repeatDep[A](p: Seq[A] => Parser[A], sep: Parser[Any]): Parser[Seq[A]] =
    {
      def loop(acc: Seq[A]): Parser[Seq[A]] = {
        val next = (sep ~> p(acc)) flatMap { result => loop(acc :+ result) }
        next ?? acc
      }
      p(Vector()) flatMap { first => loop(Seq(first)) }
    }

  /** Applies String.trim to the result of `p`. */
  def trimmed(p: Parser[String]) = p map { _.trim }

  /** Parses a URI that is valid according to the single argument java.net.URI constructor. */
  lazy val basicUri = mapOrFail(URIClass)(uri => new URI(uri))

  /** Parses a URI that is valid according to the single argument java.net.URI constructor, using `ex` as tab completion examples. */
  def Uri(ex: Set[URI]) = basicUri examples (ex.map(_.toString))
}

/** Provides standard [[Parser]] implementations. */
object Parsers extends Parsers

/** Provides common [[Parser]] implementations and helper methods.*/
object DefaultParsers extends Parsers with ParserMain {
  /** Applies parser `p` to input `s` and returns `true` if the parse was successful. */
  def matches(p: Parser[_], s: String): Boolean =
    apply(p)(s).resultEmpty.isValid

  /** Returns `true` if `s` parses successfully according to [[ID]].*/
  def validID(s: String): Boolean = {
    // matches(ID, s) would be simple but KeyIndex can call this method 100k's of times
    val length = s.length
    if (length == 0 || !s(0).isLetter) {
      return false
    }
    var i = 1
    while (i < length) {
      if (!isIDChar(s(i))) {
        return false
      }
      i += 1
    }
    true
  }
}