/* NEST (New Scala Test)
 * Copyright 2007-2013 LAMP/EPFL
 * @author Paul Phillips
 */

package scala.tools
package cmd

import scala.collection.mutable.ListBuffer

trait CommandLineConfig {
  def enforceArity: Boolean = true
  def onlyKnownOptions: Boolean = true
}

/** An instance of a command line, parsed according to a Spec.
 */
class CommandLine(val spec: Reference, val originalArgs: List[String]) extends CommandLineConfig {
  def this(spec: Reference, line: String) = this(spec, Parser tokenize line)
  def this(spec: Reference, args: Array[String]) = this(spec, args.toList)

  import spec.{ isAnyOption, isUnaryOption, isBinaryOption, isExpandOption }

  val Terminator = "--"
  val ValueForUnaryOption = "true"  // so if --opt is given, x(--opt) = true

  def mapForUnary(opt: String) = Map(opt -> ValueForUnaryOption)
  def errorFn(msg: String) = println(msg)

  /** argMap is option -> argument (or "" if it is a unary argument)
   *  residualArgs are what is left after removing the options and their args.
   */
  lazy val (argMap, residualArgs) = {
    val residualBuffer = new ListBuffer[String]

    def loop(args: List[String]): Map[String, String] = {
      def residual(xs: List[String]) = { residualBuffer ++= xs ; Map[String, String]() }

      /** Returns Some(List(args)) if this option expands to an
       *  argument list and it's not returning only the same arg.
       */
      def expand(s1: String) = {
        if (isExpandOption(s1)) {
          val s2 = spec expandArg s1
          if (s2 == List(s1)) None
          else Some(s2)
        }
        else None
      }

      /** Assumes known options have all been ruled out already. */
      def isUnknown(opt: String) =
        onlyKnownOptions && (opt startsWith "-") && {
          errorFn("Option '%s' not recognized.".format(opt))
          true
        }

      args match {
        case Nil              => Map()
        case Terminator :: xs => residual(xs)
        case x :: Nil         =>
          expand(x) foreach (exp => return loop(exp))
          if (isBinaryOption(x) && enforceArity)
            errorFn("Option '%s' requires argument, found EOF instead.".format(x))

          if (isUnaryOption(x)) mapForUnary(x)
          else if (isUnknown(x)) Map()
          else residual(args)

        case x1 :: x2 :: xs   =>
          expand(x1) foreach (exp => return loop(exp ++ args.tail))

          if (x2 == Terminator)         mapForUnary(x1) ++ residual(xs)
          else if (isUnaryOption(x1))   mapForUnary(x1) ++ loop(args.tail)
          else if (isBinaryOption(x1))  Map(x1 -> x2) ++ loop(xs)
          else if (isUnknown(x1))       loop(args.tail)
          else                          residual(List(x1)) ++ loop(args.tail)
      }
    }

    (loop(originalArgs), residualBuffer map stripQuotes toList)
  }

  def apply(arg: String)  = argMap(arg)
  def get(arg: String)    = argMap get arg
  def isSet(arg: String)  = argMap contains arg

  def getOrElse(arg: String, orElse: => String) = if (isSet(arg)) apply(arg) else orElse

  override def toString() = argMap.toString + " " + residualArgs.toString
}
