/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
  Copyright 2013-2021 Stephan Beal (https://wanderinghorse.net).

  Derived heavily from previous work:

  Copyright (c) 2013 D. Richard Hipp (https://www.hwaci.com/drh/)

  
  This program is free software; you can redistribute it and/or
  modify it under the terms of the Simplified BSD License (also
  known as the "2-Clause License" or "FreeBSD License".)
  
  This program is distributed in the hope that it will be useful,
  but without any warranty; without even the implied warranty of
  merchantability or fitness for a particular purpose.
  
  *****************************************************************************
*/

#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-cli.h"

/** Convenience form of FCLI_VN for level-3 verbosity. */
#define FCLI_V3(pfexp) FCLI_VN(3,pfexp)
#define fcli_empty_m {                     \
    NULL/*appHelp*/,                          \
    NULL/*appHelp2*/,\
      NULL/*f*/,                                \
      0/*verbose*/,                             \
      "."/*checkoutDir*/,                       \
      NULL/*argv*/,                             \
      0/*argc*/,                                \
      NULL/*appName*/,                          \
      0/*fDryRun*/,                             \
      {/*transient*/                            \
        NULL/*repoDb*/,                         \
        0/*helpRequested*/,                   \
        0/*gmtTime*/                                                  \
        },                                                            \
      {/*config*/                                                       \
        -1/*traceSql*/,                         \
        fsl_outputer_empty_m                                            \
     }                                                             \
  }


const fcli_t fcli_empty = fcli_empty_m;
fcli_t fcli = fcli_empty_m;

void fcli_printf(char const * fmt, ...){
  va_list args;
  va_start(args,fmt);
  if(fcli.f){
    fsl_outputfv(fcli.f, fmt, args);
  }else{
    fsl_fprintfv(stdout, fmt, args);
  }
  va_end(args);
}


/**
    Outputs a description of the global fcli CLI options to
    stdout.
 */
static void fcli_help_global_options();

int fcli_is_verbose(){
  return fcli.verbose;
}

fsl_cx * fcli_cx(){
  return fcli.f;
}

static int fcli_open(){
  int rc = 0;
  fsl_cx * f = fcli.f;
  assert(f);
  if(fcli.transient.repoDbArg){
    FCLI_V3(("Trying to open repo db file [%s]...\n", fcli.transient.repoDbArg));
    rc = fsl_repo_open( f, fcli.transient.repoDbArg );
  }
  else if(fcli.checkoutDir){
    fsl_buffer dir = fsl_buffer_empty;
    char const * dirName;
    rc = fsl_file_canonical_name(fcli.checkoutDir, &dir, 0);
    assert(!rc);
    dirName = (char const *)fsl_buffer_cstr(&dir);
    FCLI_V3(("Trying to open checkout from [%s]...\n",
             dirName));
    rc = fsl_checkout_open_dir(f, dirName,
                               (fsl_int_t)fsl_buffer_size(&dir), 1);
    /* if(FSL_RC_NOT_FOUND==rc) rc = FSL_RC_NOT_A_CHECKOUT; */
    if(rc){
      if(!fsl_cx_err_get(f,NULL,NULL)){
        rc = fsl_cx_err_set(f, rc, "Opening of checkout under "
                            "[%s] failed with code %d (%s).",
                            dirName, rc, fsl_rc_cstr(rc));
      }
    }
    fsl_buffer_reserve(&dir, 0);
    if(rc) return rc;
  }
  if(!rc){
    if(fcli.verbose>1){
      fsl_db * dbC = fsl_cx_db_checkout(f);
      fsl_db * dbR = fsl_cx_db_repo(f);
      if(dbC){
        FCLI_V3(("Checkout DB name: %s\n", f->ckout.db.filename));
      }
      if(dbR){
        FCLI_V3(("Opened repo db: %s\n", f->repo.db.filename));
        FCLI_V3(("Repo user name: %s\n", f->repo.user));
      }
    }
#if 0
    /*
      Only(?) here for testing purposes.

       We don't really need/want to update the repo db on each
       open of the checkout db, do we? Or do we?
     */
    fsl_repo_record_filename(f) /* ignore rc - not critical */;
#endif
  }
  return rc;
}

fsl_error * fcli_error(){
  return fcli.f ? &fcli.f->error : NULL;
}

void fcli_err_reset(){
  if(fcli.f){
    fsl_cx_err_reset(fcli.f);
  }
}


static struct {
  fsl_list list;
}  FCliFree = {
fsl_list_empty_m
};

static void fcli_shutdown(void){
  fsl_cx * f = fcli.f;
  int rc = 0;
  FCLI_V3(("Finalizing fsl_cx @%p\n", (void const *)f));

  fsl_free(fcli.argv)/*contents are in the FCliFree list*/;
  fcli = fcli_empty;

  if(f){
    if(1 &&
       fsl_cx_db_checkout(f)){
      /* For testing/demo only: this is implicit
         when we call fsl_cx_finalize().
      */
      rc = fsl_checkout_close(f);
      assert(0==rc);
      FCLI_V3(("Closed checkout/repo db(s).\n"));
    }
    fsl_cx_finalize( f );
  }
  fsl_list_clear(&FCliFree.list, fsl_list_v_fsl_free, 0);
  fsl_list_reserve(&FCliFree.list, 0);
}

void fcli_help(){
  static int hasGlobal = -99;
  if(-99==hasGlobal){
    hasGlobal = fcli_flag("global", NULL);
  }
  if(hasGlobal){
    fcli_help_global_options();
    return;
  }
  f_out("Showing app-specific help only. Use the --global option in conjunction "
        "with --help|-? to show framework-level options.\n\n");

  if(fcli.appHelp2){
    fcli_help_t const * h = fcli.appHelp2;
    fcli_help_arg_t const * flag = h->flags;
    f_out("%s\n\n", h->briefDescription);
    f_out("Usage:\n\t%s %s\n", fcli.appName, h->briefUsage ? h->briefUsage : "");
    if(flag) f_out("\nOptions:\n");
    for( ; flag->flagShort || flag->flagLong; ++flag ){
      f_out("\t");
      if(flag->valType){
        if(flag->flagLong){
          if(flag->flagShort){
            f_out("--%s|-%s=%s", flag->flagLong,
                  flag->flagShort, flag->valType);
          }else{
            f_out("--%s=%s", flag->flagLong, flag->valType);
          }
        }else{
          assert(flag->flagShort);
          f_out("-%s=%s", flag->flagShort, flag->valType);
        }
      }else{
        if(flag->flagLong){
          if(flag->flagShort){
            f_out("--%s|-%s", flag->flagLong,
                  flag->flagShort);
          }else{
            f_out("--%s%s", flag->flagLong,
                  flag->flagShort);
          }
        }else{
          assert(flag->flagShort);
          f_out("-%s", flag->flagShort);
        }
      }
        f_out("\n\t", flag->brief);
      if(flag->brief){
        f_out("%s\n\n", flag->brief);
      }else if(flag->callback){
        flag->callback();
      }
    }
    if(h->callback){
      h->callback();
    }
  }else if(fcli.appHelp){
    fcli.appHelp();
  }
}
  

static int fcli_process_args( int argc, char * const * argv ){
  int i;
  int rc = 0;
  char * cp;
  fcli.appName = argv[0];
  fcli.argc = 0;
  fcli.argv = (char **)fsl_malloc( (argc + 1) * sizeof(char*));
  fcli.argv[argc] = NULL;
  for( i = 1; i < argc; ++i ){
    char const * arg = argv[i];
    if('-'==*arg){
      char const * flag = arg+1;
      while('-'==*flag) ++flag;
#define FLAG(F) if(0==fsl_strcmp(F,flag))
      FLAG("help") {
        fcli.transient.helpRequested = 1;
        continue;
      }
      FLAG("?") {
        fcli.transient.helpRequested = 1;
        continue;
      }
      FLAG("V") {
        fcli.verbose += 1;
        continue;
      }
      FLAG("VV") {
        fcli.verbose += 2;
        continue;
      }
      FLAG("VVV") {
        fcli.verbose += 3;
        continue;
      }
      FLAG("verbose") {
        fcli.verbose += 1;
        continue;
      }
      FLAG("C"){ /* Disable auto-open of checkout */
        fcli.checkoutDir = NULL;
        continue;
      }
      FLAG("S") {
        fcli.config.traceSql = 1;
        continue;
      }
#undef FLAG
      /* else fall through */
    }
    cp = fsl_strdup(arg);
    if(!cp) return FSL_RC_OOM;
    fcli.argv[fcli.argc++] = cp;
    fcli_fax(cp);
  }

  /** Now pull out our flags. We do this
      in a second step so that we can use
      the fcli_flag() parsing.
  */
  if( fcli_flag("no-checkout", NULL) ){
    fcli.checkoutDir = NULL;
  }
  if(fcli.config.traceSql<0){
    fcli.config.traceSql = fcli_flag("trace-sql", NULL);
  }else{
    /* Just make sure it's not left around in the args list. */
    fcli_flag("trace-sql", NULL);
  }
  fcli.fDryRun = fcli_flag("dry-run", NULL);
  fcli_flag2("R", "repo", &fcli.transient.repoDbArg);
#if 0
  fcli.transient.gmtTime = fcli_flag("gmt", NULL);
#endif

  /*
    Keep 'help' LAST so that we can can use the first-non-flag==help
    mechanism.
  */
  if(fcli.argc && 0==fsl_strcmp("help",fcli.argv[0])){
    fcli_next_arg(1) /* strip argument */;
    fcli.transient.helpRequested = 2;
  }
  return rc;
}

char fcli_flag(char const * opt, const char ** value){
  int i = 0;
  int remove = 0 /* number of items to remove from argv */;
  char rc = 0 /* true if found, else 0 */;
  fsl_size_t optLen = fsl_strlen(opt);
  for( ; i < fcli.argc; ++i ){
    char const * arg = fcli.argv[i];
    char const * x;
    char const * vp = NULL;
    if(!arg || ('-' != *arg)) continue;
    rc = 0;
    x = arg+1;
    if('-' == *x) { ++x;}
    if(0 != fsl_strncmp(x, opt, optLen)) continue;
    if(!value){
      if(x[optLen]) continue /* not exact match */;
      /* Treat this as a boolean. */
      rc = 1;
      ++remove;
      break;
    }else{
      /* -FLAG VALUE or -FLAG=VALUE */
      if(x[optLen] == '='){
        rc = 1;
        vp = x+optLen+1;
        ++remove;
      }
      else if(x[optLen]) continue /* not an exact match */;
      else if(i<(fcli.argc-1)){ /* -FLAG VALUE */
        rc = 1;
        vp = fcli.argv[i+1];
        remove += 2;
      }
      else{
        /*
          --FLAG is expecting VALUE but we're at end of argv.  Leave
          --FLAG in the args and report this as "not found."
        */
        rc = 0;
        assert(!remove);
      }
      if(rc){
        *value = vp;
      }
      break;
    }
  }
  if(remove>0){
    int x;
    for( x = 0; x < remove; ++x ){
      fcli.argv[i+x] = NULL/*memory ownership==>FCliFree*/;
    }
    for( ; i < fcli.argc; ++i ){
      fcli.argv[i] = fcli.argv[i+remove];
    }
    fcli.argc -= remove;
    fcli.argv[i] = NULL;
  }
  return rc;
}

char fcli_flag2(char const * shortOpt,
                         char const * longOpt,
                         const char ** value){
  char const rc = fcli_flag(shortOpt, value);
  return (rc || !longOpt) ? rc : fcli_flag(longOpt, value);
}

char fcli_flag_or_arg(char const * shortOpt,
                      char const * longOpt,
                      const char ** value){
  char rc = fcli_flag(shortOpt, value);
  if(!rc){
    rc = fcli_flag(longOpt, value);
    if(!rc && value){
      const char * arg = fcli_next_arg(1);
      if(arg){
        rc = 1;
        *value = arg;
      }
    }
  }
  return rc;
}


/**
    We copy fsl_lib_configurable.allocator as a base allocator.
 */
static fsl_allocator fslAllocOrig;

/**
    Proxies fslAllocOrig() and abort()s on OOM conditions.
 */
static void * fsl_realloc_f_failing(void * state, void * mem, fsl_size_t n){
  void * rv = fslAllocOrig.f(fslAllocOrig.state, mem, n);
  if(n && !rv){
    fprintf(stderr,"\nfcli: OUT OF MEMORY\n");
    fflush(stderr);
    abort();
  }
  return rv;
}

/**
    Replacement for fsl_memory_allocator() which abort()s on OOM.
    Why? Because fossil(1) has shown how much that can simplify error
    checking in an allocates-often API.
 */
static const fsl_allocator fcli_allocator = {
fsl_realloc_f_failing,
NULL/*state*/
};

int fcli_setup(int argc, char * const * argv ){
  static char once = 0;
  fsl_cx * f = NULL;
  int rc = 0;
  fsl_cx_init_opt init = fsl_cx_init_opt_empty;
  if(once){
    fprintf(stderr,"MISUSE: fcli_setup() must "
            "not be called more than once.");
    return FSL_RC_MISUSE;
  }
  init.config.sqlPrint = 1;
  once = 1;
  fslAllocOrig = fsl_lib_configurable.allocator;
  fsl_lib_configurable.allocator = fcli_allocator
    /* This MUST be done BEFORE the fsl API allocates
       ANY memory!
    */;
  atexit(fcli_shutdown);
  rc = fcli_process_args(argc, argv);
  if(rc) return rc;

  if(fcli.transient.helpRequested){
    /* Do this last so that we can get the default user name and such
       for display in the help text. */
    fcli_help();
    rc = FSL_RC_BREAK;
  }
  else{
    if(fcli.config.outputer.out){
      init.output = fcli.config.outputer;
      fcli.config.outputer = fsl_outputer_empty
        /* To avoid any confusion about ownership */;
    }else{
      init.output = fsl_outputer_FILE;
      init.output.state.state = stdout;
    }
    init.config.traceSql = fcli.config.traceSql;
    
    rc = fsl_cx_init( &f, &init );
    fcli.f = f;
    FCLI_V3(("Initialized fsl_cx @0x%p. rc=%s\n",
              (void const *)f, fsl_rc_cstr(rc)));
    if(!rc){
#if 0
      if(fcli.transient.gmtTime){
        fsl_cx_flag_set(f, FSL_CX_F_LOCALTIME_GMT, 1);
      }
#endif
      if(fcli.checkoutDir || fcli.transient.repoDbArg){
        rc = fcli_open();
        if(!fcli.transient.repoDbArg && fcli.checkoutDir
           && (FSL_RC_NOT_FOUND == rc)){
          /* If [it looks like] we tried an implicit checkout-open but
             didn't find one, suppress the error.
          */
          rc = 0;
          fcli_err_reset();
        }
      }
    }
  }
  if(!rc){
    char const * userName = NULL;
    fcli_flag2("user", "U", &userName);
    if(userName){
      fsl_cx_user_set(f, userName);
    }else if(!fsl_cx_user_get(f)){
      char * u = fsl_guess_user_name();
      fsl_cx_user_set(f, u);
      fsl_free(u);
    }
  }
  return rc;
}


void fcli_help_global_options(){

  char const * user = fsl_cx_user_get(fcli.f);
  puts("Global fcli options and features:\n");
  puts("\t--help|-? or the word 'help' as the first non-flag argument "
       "triggers this help message.\n");
  puts("\t--dry-run enables 'dry-run' mode for some (not all) apps\n");
#if 0
  puts("\t--gmt sets the FSL_CX_F_LOCALTIME_GMT flag.\n");
#endif
  puts("\t--no-checkout|-C supresses the automatic attempt "
       "to open a checkout under the current directory. "
       "This is implied by -R, to avoid opening a "
       "mismatched checkout/repo combination.\n");
  puts("\t--repo|-R=DBFILE specifies a repository "
       "db to open.\n");
  puts("\t--trace-sql|-S enables SQL tracing.\n");
  printf("\t--user|-U=name [current=%s] sets the default Fossil "
         "user for operations which need one.\n\n", user);
  puts("\t--verbose|-V enables verbose mode "
       "(possibly generating extra output). "
       "Use multiple times for more verbosity. "
       "Shortcuts: -VV and -VVV.\n");
  puts("All flags may be preceeded by - or -- and "
       "flags taking values may be in the form -FLAG=VALUE "
       "or -FLAG VALUE. Flags not taking values are "
       "treated as booleans.\n");
}

int fcli_err_report2(char clear, char const * file, int line){
  int errRc = 0;
  if(fcli.f){
    char const * msg = NULL;
    errRc = fsl_cx_err_get( fcli.f, &msg, NULL );
    if(errRc || msg){
      if(fcli.verbose>0){
        fcli_printf("%s %s:%d: ERROR #%d (%s): %s\n",
                    fcli.appName,
                    file, line, errRc, fsl_rc_cstr(errRc), msg);
      }else{
        fcli_printf("%s: ERROR #%d (%s): %s\n",
                    fcli.appName, errRc, fsl_rc_cstr(errRc), msg);
      }
    }
    if(clear) fcli_err_reset();
  }
  return errRc;
}


const char * fcli_next_arg(int remove){
  const char * rc = (fcli.argc>0) ? fcli.argv[0] : NULL;
  if(rc && remove){
    int i;
    --fcli.argc;
    for(i = 0; i < fcli.argc; ++i){
      fcli.argv[i] = fcli.argv[i+1];
    }
    fcli.argv[fcli.argc] = NULL/*owned by FCliFree*/;
  }
  return rc;
}

int fcli_has_unused_flags(int outputError){
  int i;
  for( i = 0; i < fcli.argc; ++i ){
    char const * arg = fcli.argv[i];
    if('-'==*arg){
      fsl_cx_err_set(fcli.f, FSL_RC_MISUSE,
                     "Unhandled/unknown flag: %s",
                     arg);
      if(outputError){
        fcli_err_report(0);
      }
      return 1;
    }
  }
  return 0;
}

int fcli_err_set(int code, char const * fmt, ...){
  int rc;
  va_list va;
  va_start(va, fmt);
  rc = fsl_cx_err_setv(fcli.f, code, fmt, va);
  va_end(va);
  return rc;
}

int fcli_end_of_main(int mainRc){
  return (fcli_err_report(1)||mainRc) ? EXIT_FAILURE : EXIT_SUCCESS;
}


int fcli_dispatch_commands( FossilCommand const * cmd,
                            char reportErrors){
  int rc = 0;
  const char * arg = fcli_next_arg(0);
  FossilCommand const * orig = cmd;
  if(!arg){
    return fcli_err_set(FSL_RC_MISUSE,
                        "Missing command argument. Try --help.");
  }
  assert(fcli.f);
  for( ; arg && cmd->name; ++cmd ){
    if(0==fsl_strcmp(arg,cmd->name)){
      if(!cmd->f){
        rc = fcli_err_set(FSL_RC_NYI,
                               "Command [%s] has no "
                               "callback function.");
      }else{
        fcli_next_arg(1);
        rc = cmd->f();
      }
      break;
    }
  }
  if(!cmd->name){
    fsl_buffer msg = fsl_buffer_empty;
    int rc2;
    if(!arg){
      rc2 = FSL_RC_MISUSE;
      fsl_buffer_appendf(&msg, "No command provided.");
    }else{
      rc2 = FSL_RC_NOT_FOUND;
      fsl_buffer_appendf(&msg, "Command not found: %s.",arg);
    }
    fsl_buffer_appendf(&msg, " Available commands: ");
    cmd = orig;
    for( ; cmd && cmd->name; ++cmd ){
      fsl_buffer_appendf( &msg, "%s%s",
                          (cmd==orig) ? "" : ", ",
                          cmd->name);
    }
    rc = fcli_err_set(rc2, "%b", &msg);
    fsl_buffer_clear(&msg);
  }
  if(rc && reportErrors){
    fcli_err_report(0);
  }
  return rc;
}

void fcli_fax(void * mem){
  if(mem){
    fsl_list_append( &FCliFree.list, mem );
  }
}

#undef FCLI_V3
#undef fcli_empty_m
