//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//    3 May 11  Brian Frank  Creation
//

**
** Command implements a top-level command in the fanr command line tool.
**
** Commands declare their options using the `CommandOpt` facet which
** works similiar to `util::AbstractMain`.  If the field is a Bool, then
** the option is treated as a flag option.  Otherwise it must be one of
** these types: Str, Uri.
**
abstract class Command
{

//////////////////////////////////////////////////////////////////////////
// Overrides
//////////////////////////////////////////////////////////////////////////

  ** Name of command
  abstract Str name()

  ** Short summary of command for usage screen
  abstract Str summary()

  ** Execute command.  If there is a failure then throw `err`,
  ** otherwise the command is assumed to be successful.
  abstract Void run()

//////////////////////////////////////////////////////////////////////////
// Output
//////////////////////////////////////////////////////////////////////////

  ** Stdout for printing command output
  OutStream out := Env.cur.out

  ** Log a warning to `out`
  Void warn(Str msg)
  {
    out.printLine("WARN: $msg")
  }

  ** Throw an exception which may be used to unwind the stack
  ** back to main to indicate command failed and return non-zero
  Err err(Str msg, Err? cause := null)
  {
    return CommandErr(msg, cause)
  }

  ** Ask for y/n confirmation or skip if '-y' option specified.
  Bool confirm(Str msg)
  {
    if (skipConfirm) return true
    out.printLine
    out.print("$msg [y/n]: ").flush
    r := Env.cur.in.readLine
    return r.lower.startsWith("y")
  }

  ** Pretty print a pod versions to output stream
  internal Void printPodVersion(PodSpec version)
  {
    printPodVersions([version])
  }

  ** Pretty print a list of pod versions (of same pod) to output stream
  internal Void printPodVersions(PodSpec[] versions)
  {
    top := versions.sortr.first

    // ensure summary isn't too long
    summary := top.summary
    if (summary.size > 100) summary = summary[0..100] + "..."

    // figure out alignment padding for versions
    verPad := 6
    versions.each |x| { verPad = verPad.max(x.version.toStr.size) }

    // print it
    out.printLine(top.name)
    out.printLine("  $summary")
    versions.each |x|
    {
      // build details as "ts, size"
      details := StrBuf()
      if (x.ts != null) details.join(x.ts.date.toLocale("DD-MMM-YYYY"), ", ")
      if (x.size != null) details.join(x.size.toLocale("B"), ", ")

      // print version info line
      verStr := x.version.toStr.padr(verPad)
      out.printLine("  $verStr ($details)")
    }
  }

//////////////////////////////////////////////////////////////////////////
// Global Options
//////////////////////////////////////////////////////////////////////////

  ** Repository URI -r option
  @CommandOpt
  {
    name   = "r"
    help   = "Repository URI for command"
    config = "repo"
  }
  Uri? repoUri

  ** Get the repo to use for this command:
  **   - default is config prop "repo"
  **   - override with "-r" option
  once Repo repo()
  {
    if (repoUri == null)
      throw err("No repoUri available: use -r or set 'repo' in etc/fanr/config.props")

    try
      return Repo.makeForUri(repoUri, username, password)
    catch (Err e)
      throw err("Cannot init repo: $repoUri", e)
  }

  ** Get the local environment to use this command
  once FanrEnv env() { FanrEnv() }

  ** Option to dump full stack trace on errors
  @CommandOpt
  {
    name   = "errTrace"
    help   = "Dump error stack traces"
  }
  Bool errTrace

  ** Option to skip confirmation (auto yes)
  @CommandOpt
  {
    name   = "y"
    help   = "Skip confirmation"
  }
  Bool skipConfirm

  ** Username for authentication
  @CommandOpt
  {
    name   = "u"
    help   = "Username for authentication"
    config = "username"
  }
  Str? username

  ** Password for authentication
  @CommandOpt
  {
    name   = "p"
    help   = "Password for authentication"
    config = "password"
  }
  Str? password

//////////////////////////////////////////////////////////////////////////
// Initialization
//////////////////////////////////////////////////////////////////////////

  internal Bool init(Str[] args)
  {
    initOptsFromConfig
    if (!parseArgs(args)) return false
    promptPassword
    return true
  }

  private Void initOptsFromConfig()
  {
    optFields.each |field|
    {
      val := optDefault(field)
      field.set(this, val)
    }
  }

  private Obj? optDefault(Field field)
  {
    def := field.get(this)
    CommandOpt facet := field.facet(CommandOpt#)
    if (facet.config != null)
    {
      config := Command#.pod.config(facet.config)
      if (config != null)
      {
        try
          def = parseVal(field.type, config)
        catch (Err e)
        err("Invalid config value for '$facet.config': $config")
      }
    }
    return def
  }

  private Bool parseArgs(Str[] toks)
  {
    args := argFields
    opts := optFields
    varArgs := !args.isEmpty && args.last.type.fits(List#)
    argi := 0
    for (i:=0; i<toks.size; ++i)
    {
      tok := toks[i]
      Str? next := i+1 < toks.size ? toks[i+1] : null
      if (tok.startsWith("-"))
      {
        if (parseOpt(opts, tok, next)) ++i
      }
      else if (argi < args.size)
      {
        if (parseArg(args[argi], tok)) ++argi
      }
      else
      {
        warn("Unexpected arg: $tok")
      }
    }
    if (argi == args.size) return true
    if (argi == args.size-1 && varArgs) return true

    // // missing args
    usage
    out.printLine
    out.printLine("Missing arguments")
    return false
  }

  private Field[] argFields()
  {
    Type.of(this).fields.findAll |f| { f.hasFacet(CommandArg#) }
  }

  private Field[] optFields()
  {
    Type.of(this).fields.findAll |f| { f.hasFacet(CommandOpt#) }
  }

  private Bool parseOpt(Field[] opts, Str tok, Str? next)
  {
    n := tok[1..-1]
    for (i:=0; i<opts.size; ++i)
    {
      // if name doesn't match opt or any of its aliases then continue
      field := opts[i]
      facet := (CommandOpt)field.facet(CommandOpt#)
      if (facet.name != n) continue

      // if field is a bool we always assume the true value
      if (field.type == Bool#)
      {
        field.set(this, true)
        return false // did not consume next
      }

      // check that we have a next value to parse
      if (next == null || next.startsWith("-"))
      {
        err("Missing value for -$n")
        return false // did not consume next
      }

      try
      {
        // parse the value to proper type and set field
        field.set(this, parseVal(field.type, next))
      }
      catch (Err e) err("Cannot parse -$n as $field.type.name: $next")
      return true // we *did* consume next
    }

    warn("Unknown option -$n")
    return false // did not consume next
  }

  private Bool parseArg(Field field, Str tok)
  {
    isList := field.type.fits(List#)
    try
    {
      // if not a list, this is easy
      if (!isList)
      {
        field.set(this, parseVal(field.type, tok))
        return true // increment argi
      }

      // if list, then parse list item and add to end of list
      of := field.type.params["V"]
      val :=  parseVal(of, tok)
      list := field.get(this) as Obj?[]
      if (list == null) field.set(this, list = List.make(of, 8))
      list.add(val)
    }
    catch (Err e) err("Cannot parse argument as $field.type.name: $tok")
    return !isList // increment argi if not list
  }

  private static Obj? parseVal(Type of, Str tok)
  {
    of = of.toNonNullable
    if (of == Str#) return tok
    if (of == File#) return parsePath(tok)
    return of.method("fromStr").call(tok)
  }

  internal static File parsePath(Str path)
  {
    if (path.contains("\\"))
      return File.os(path).normalize
    else
      return File.make(path.toUri, false)
  }

  private Void promptPassword()
  {
    // if we have a username, but no password then prompt for it
    if (username != null && password == null)
      password = Env.cur.promptPassword("Password for '$username'>")
  }

//////////////////////////////////////////////////////////////////////////
// Usage
//////////////////////////////////////////////////////////////////////////

  ** Print usage to given output stream
  virtual Void usage(OutStream out := this.out)
  {
    // get list of argument and option fields
    args := argFields
    opts := optFields

    // format args/opts into columns
    argRows := usagePad(args.map |f| { usageArg(f) })
    optRows := usagePad(opts.map |f| { usageOpt(f) })

    // format summary line
    argSummary := args.join(" ") |field|
    {
      CommandArg facet := field.facet(CommandArg#)
      s := "<" + facet.name + ">"
      if (field.type.fits(List#)) s += "*"
      return s
    }

    out.printLine("Summary:")
    out.printLine("  $summary")
    out.printLine("Usage:")
    out.printLine("  fanr $name [options] $argSummary")
    usagePrint(out, "Arguments:", argRows)
    usagePrint(out, "Options:", optRows)
  }

  private Str[] usageArg(Field field)
  {
    CommandArg facet := field.facet(CommandArg#)
    return [facet.name, facet.help]
  }

  private Str[] usageOpt(Field field)
  {
    CommandOpt facet := field.facet(CommandOpt#)
    name := facet.name
    def  := optDefault(field)
    help := facet.help

    col1 := "-$name"
    if (def != false) col1 += " <$field.type.name>"

    col2 := help
    if (def != false && def != null) col2 += " (default $def)"

    return [col1, col2]
  }

  private Str[][] usagePad(Str[][] rows)
  {
    if (rows.isEmpty) return rows
    Int max := rows.map |row| { row[0].size }.max
    pad := 20.min(2 + max)
    rows.each |row| { row[0] = row[0].padr(pad) }
    return rows
  }

  private Void usagePrint(OutStream out, Str title, Str[][] rows)
  {
    if (rows.isEmpty) return
    out.printLine(title)
    rows.each |row| { out.printLine("  ${row[0]}  ${row[1]}") }
  }

}

**************************************************************************
** CommandErr
**************************************************************************

**
** CommandErr is used to unwind the stack back to main
**
internal const class CommandErr : Err
{
  new make(Str msg, Err? cause) : super(msg, cause) {}
}

**************************************************************************
** CommandArg
**************************************************************************

**
** Facet for annotating an `Command` argument field.
**
facet class CommandArg
{
  ** Name of the argument
  const Str name

  ** Usage help, should be a single short line summary
  const Str help
}

**************************************************************************
** CommandOpt
**************************************************************************

**
** Facet for annotating an `Command` option field.
**
facet class CommandOpt
{
  ** Name of option to use on command line
  const Str name

  ** Usage help, should be a single short line summary
  const Str help

  ** Property name to use to initialize from fanr config
  const Str? config
}