//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   8 Mar 08  Brian Frank  Creation
//   1 Dec 09  Brian Frank  Rename BootScript to AbstractMain
//

using concurrent

**
** AbstractMain provides conveniences for writing the main routine
** of an app. Command line arguments are configured as fields
** with the `Arg` facet:
**
**   @Arg { help = "source file to process" }
**   File? src
**
** Arguments are ordered by the field declaration order.  The
** last argument may be declared as a list to handle a variable
** numbers of arguments.
**
** Command line options are configured as fields with
** the `Opt` facet :
**
**   @Opt { help = "http port"; aliases=["p"] }
**   Int port := 8080
**
** Bool fields should always default to false and are considered
** flag options.  All other arg and opt fields must be a Str, File,
** or have a type which supports a 'fromStr' method.
**
** Option fields may include the "Opt" suffix, and arguments the
** "Arg" suffix.  These suffixes can be used when a field conflicts
** with an existing slot name.
**
** AbstractMain will automatically implement `usage` and
** `parseArgs` based on the fields which are declared as options
** and arguments.  In general subclasses only need to override `run`.
** If writing a daemon main, then you'll probably want to configure
** your services then call `runServices`.
**
** See [example]`examples::util-main`.
**
abstract class AbstractMain
{

//////////////////////////////////////////////////////////////////////////
// Environment
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the application name.  If this is a script it is the
  ** name of the script file.  For a precompiled class called
  ** "Main" this is the pod name, otherwise it is the type name.
  **
  virtual Str appName()
  {
    t := Type.of(this)
    if (t.pod.meta["pod.isScript"] == "true") return t->sourceFile->toUri->basename
    if (t.name == "Main") return t.pod.name
    return t.name
  }

  **
  ** Log for this application; defaults to `appName`.
  **
  virtual Log log() { Log.get(appName) }

  **
  ** Home directory for the application.  For a script
  ** this defaults to directory of the script.  For pods
  ** the default is "{Env.cur.workDir}/etc/{pod}".
  **
  virtual File homeDir()
  {
    t := Type.of(this)
    if (t.pod.meta["pod.isScript"] == "true")
      return File(t->sourceFile->toUri).parent
    else
      return Env.cur.workDir + `etc/${t.pod.name}/`
  }

  **
  ** Print usage help.
  **
  @Opt { help = "Print usage help"; aliases = ["?"] }
  Bool helpOpt := false

//////////////////////////////////////////////////////////////////////////
// Command Line
//////////////////////////////////////////////////////////////////////////

  **
  ** Get all the fields annotated with the [@Arg]`Arg` facet.
  **
  virtual Field[] argFields()
  {
    Type.of(this).fields.findAll |f| { f.hasFacet(Arg#) }
  }

  **
  ** Get all the fields annotated with the [@Opt]`Opt` facet.
  **
  virtual Field[] optFields()
  {
    Type.of(this).fields.findAll |f| { f.hasFacet(Opt#) }
  }

  **
  ** Parse the command line and set this instances fields.
  ** Return false if not all of the arguments were passed.
  **
  virtual 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
      {
        log.warn("Unexpected arg: $tok")
      }
    }
    if (argi == args.size) return true
    if (argi == args.size-1 && varArgs) return true
    return false // missing args
  }

  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 := (Opt)field.facet(Opt#)
      aliases := facet
      if (optName(field) != n && !facet.aliases.contains(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("-"))
      {
        log.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) log.err("Cannot parse -$n as $field.type.name: $next")
      return true // we *did* consume next
    }

    log.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) log.err("Cannot parse argument as $field.type.name: $tok")
    return !isList // increment argi if not list
  }

  private Str argName(Field f)
  {
    if (f.name.endsWith("Arg")) return f.name[0..<-3]
    return f.name
  }

  private Str optName(Field f)
  {
    if (f.name.endsWith("Opt")) return f.name[0..<-3]
    return f.name
  }

  private Obj? parseVal(Type of, Str tok)
  {
    of = of.toNonNullable
    if (of == Str#) return tok
    if (of == File#)
    {
      if (tok.contains("\\"))
        return File.os(tok).normalize
      else
        return File.make(tok.toUri, false)
    }
    return of.method("fromStr").call(tok)
  }

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

  **
  ** Print usage of arguments and options.
  ** Return non-zero.
  **
  virtual Int usage(OutStream out := Env.cur.out)
  {
    // get list of argument fields
    args := argFields

    // get list of all documented options
    opts := optFields.findAll |f| { !f.hasFacet(NoDoc#) }

    // 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|
    {
      s := "<" + argName(field) + ">"
      if (field.type.fits(List#)) s += "*"
      return s
    }

    // print usage
    out.printLine("Usage:")
    out.printLine("  $appName [options] $argSummary")
    usagePrint(out, "Arguments:", argRows)
    usagePrint(out, "Options:", optRows)
    return 1
  }

  private Str[] usageArg(Field field)
  {
    name := argName(field)
    help := field.facet(Arg#)->help as Str
    return [name, help]
  }

  private Str[] usageOpt(Field field)
  {
    name := optName(field)
    def  := field.get(Type.of(this).make)
    help := field.facet(Opt#)->help as Str
    Str[] aliases := field.facet(Opt#)->aliases

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

    col2 := help
    if (def != false && def != null && def != "") 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]}") }
  }

//////////////////////////////////////////////////////////////////////////
// Run
//////////////////////////////////////////////////////////////////////////

  **
  ** Run the application.  This method is called after the
  ** command line has been parsed.  See `runServices` to
  ** launch a deamon application.  Return status code, zero
  ** for success.
  **
  abstract Int run()

  **
  ** Run the set of services:
  **   1. call install on each service
  **   2. call start on each service
  **   3. put main thread to sleep.
  **   4. on shutodwn call stop on each service
  **   5. then call uninstall on each service
  **
  virtual Int runServices(Service[] services)
  {
    Env.cur.addShutdownHook |->| { shutdownServices }
    services.each |Service s| { s.install }
    services.each |Service s| { s.start }
    Actor.sleep(Duration.maxVal)
    return 0
  }

  private static Void shutdownServices()
  {
    Service.list.each |Service s| { s.stop }
    Service.list.each |Service s| { s.uninstall }
  }

//////////////////////////////////////////////////////////////////////////
// Main
//////////////////////////////////////////////////////////////////////////

  **
  ** Main performs the following tasks:
  **   1. Calls `parseArgs` to parse command line
  **   2. If args were incomplete or -help was specified
  **      then dump usage and return 1
  **   3. Call `run` and return 0
  **   4. If an exception is raised log it and return 1
  **
  virtual Int main(Str[] args := Env.cur.args)
  {
    success := false
    try
    {
      // parse command line
      argsOk := parseArgs(args)

      // if args not ok or help was specified, dump usage
      if (!argsOk || helpOpt)
      {
        out := Env.cur.out
        out.printLine
        usage(out)
        out.printLine
        if (!helpOpt) log.err("Missing arguments")
        return 1
      }

      // call run
      return run
    }
    catch (Err err)
    {
      log.err("Cannot boot")
      err.trace
      return 1
    }
  }

}

**************************************************************************
** Arg
**************************************************************************

**
** Facet for annotating an `AbstractMain` argument field.
**
facet class Arg
{
  ** Usage help, should be a single short line summary
  const Str help := ""
}

**************************************************************************
** Opt
**************************************************************************

**
** Facet for annotating an `AbstractMain` option field.
**
facet class Opt
{
  ** Usage help, should be a single short line summary
  const Str help := ""

  ** Aliases for the option
  const Str[] aliases := Str[,]
}