/* -*- 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 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt

  SPDX-License-Identifier: BSD-2-Clause-FreeBSD
  SPDX-FileCopyrightText: 2021 The Libfossil Authors
  SPDX-ArtifactOfProjectName: Libfossil
  SPDX-FileType: Code

  Heavily indebted to the Fossil SCM project (https://fossil-scm.org).
*/
/*
  *****************************************************************************
  This file implements the code to open and checkout a Fossil repository.
*/

#include "fossil-scm/fossil-cli.h"  /* Fossil App mini-framework */
#include "fossil-scm/fossil-core.h"
#include "fossil-scm/fossil-db.h"
#include "fossil-scm/fossil-internal.h"
#include "fossil-scm/fossil-util.h"

#include <errno.h>
#include <sys/stat.h>
#define MARKER(pfexp)                                               \
  do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__);   \
    printf pfexp;                                                   \
  } while(0)

static int verbose = 0;

typedef struct {
  int  extracted;
  int  kept;
  bool keep;
  bool quiet;
} extract_state;

static int
fsl_repo_ckout_f_my(fsl_repo_ckout_state const *cState)
{
  enum {
    DATE_SHORT = 16
  };
  fsl_repo_extract_state const * xs = cState->extractState;
  fsl_cx        *f = xs->f;
  extract_state  *state = (extract_state *)cState->callbackState;
  static char    tbuf[DATE_SHORT];
  int            rc = 0;
  char mode = '?';
  assert(f);
  assert(xs->fCard->uuid);
  if(cState->fileWasWritten) {
    mode = '+';
    ++state->extracted;
  }else{
    mode = '!';
    ++state->kept;
  }
  if(!state->quiet){
    fsl_strftime_unix(tbuf, DATE_SHORT, "%d %b %Y", cState->mtime, 1);
    f_out("[%c] %8"FSL_SIZE_T_PFMT"   %s  %s\n",
          mode, cState->size, tbuf, xs->fCard->name);
  }
  return rc;
}

int
main(int argc, char const * const *argv)
{
  fsl_buffer          buf = fsl_buffer_empty;
  fsl_db             *db = 0;
  fsl_db             *dbRepo = 0;
  extract_state       ex = {0,0,false};
  fsl_cx             *f = 0;
  fsl_repo_open_ckout_opt   opt = fsl_repo_open_ckout_opt_empty;
  fsl_id_t            rv = 0, prev_ckout = 0;
  const char         *repodir =0, *repository =0, *workdir = 0;
  const char         *sym = NULL;
  char               cwd[FILENAME_MAX];
  bool               empty = false, force = false, keep = false,
    /*manifest = false,*/ mtime = false, nested = false, q = false,
    uri = false;
  int                rc = 0;
  
  fcli_cliflag       const fcli_flags[] = {
    FCLI_FLAG_BOOL("E", "empty", &empty,
     "Initialise checkout as being empty. The checkout will be connected to "
     "the local repository but no files will be written to disk."),
    FCLI_FLAG_BOOL("f", "force", &force, "Continue opening the <repository> if "
     "the working directory is not empty. Files present on disk with the same "
     "name as files from the requested <version> being checked out will be "
     "overwritten, other files will remain untouched."),
    FCLI_FLAG_BOOL(0, "keep", &keep, "Only checkout files from the requested "
     "<version> that do not have a file of the same name already present on "
     "disk. Files with the same name as those from the requested <version> will"
     " remain unmodified irrespective of whether their content is consistent "
     "with that of the requested <version>. In such a case, the checkout will "
     "immediately be in a changed state, which 'f-status' will report."),
#if 0
    // Not implemented
    FCLI_FLAG_BOOL(0, "manifest", &manifest,
     "Only modify the manifest and manifest.uuid files."),
#endif
    FCLI_FLAG_BOOL(0, "nested", &nested,
     "Allow opening the <repository> inside an opened checkout."),
    FCLI_FLAG_BOOL("q", "quiet", &q,
     "Suppress non-error output unless --verbose is used."),
    /*FCLI_FLAG(0, "repodir", "<dir>", &repodir,
     "If <repository> is a URI that will be cloned, store the clone in <dir> "
     "rather than the current working directory."),
    */
    FCLI_FLAG_BOOL(0, "setmtime", &mtime,
                   "Set timestamps of all files to that "
                   "of the last check-in in which they were modified "
                   "(i.e., manifest time)."),
    FCLI_FLAG(0, "workdir", "<dir>", &workdir,
     "Open the checkout into <dir> instead of the current working directory. "
     "This option will create <dir> if it does not already exist."),
    fcli_cliflag_empty_m
  };
  fcli_help_info const fcli_help = {
    "Open a new connection to the <repository>. A checkout of the most recent "
    "check-in is created if no <version> is specified.",
    "<repository> [<version>]",
    NULL
  };

  fcli.checkoutDir = NULL;  /* Cannot open yet; we're making it now. */
  fcli.cliFlags = fcli_flags;
  fcli.appHelp = &fcli_help;
  rc = fcli_setup(argc, argv);
  if(rc) goto end;
  f = fcli_cx();
  assert(!fsl_cx_db_repo(f));

  verbose = fcli_is_verbose();
  if(!verbose){
    verbose = !q;
  }

  if(fcli_has_unused_flags(0)){
    goto end;
  }

  dbRepo = fsl_cx_db_repo(f);
  if(dbRepo){
    // Was opened via -R flag.
    repository = fsl_cx_db_file_repo(f, 0);
  }else{
    repository = fcli_next_arg(1);
    if (!repository) {
      rc = fcli_err_set(FSL_RC_MISUSE,
                        "Usage: %s [options] <repository> [<version>]\n"
                        "Or try --help", fcli.appName);
      goto end;
    }
    if (fsl_str_glob("http://*", repository)
        || fsl_str_glob("https://*", repository)
        || fsl_str_glob("ssh:*", repository)
        || fsl_str_glob("file:*", repository)){
      uri = true;
      rc = fcli_err_set(FSL_RC_UNSUPPORTED,"URI-style names are "
                        "not currently supported.");
      goto end;
    }
  }
  assert(repository);

  if(workdir && dbRepo){
    MARKER(("FIXME: --workdir is currently incompatible with -R <REPO> "
            "handling because of a mismatch in path translation.\n"))
    /* ^^^ specifically, we need -R to be able to use a relative path
       so that the relative path gets stored in the ckout.vvar.repository
       setting. --workdir will, however, change how a relative path
       is interpreted. */;
  }else if(workdir) {
    char * zTmp = 0;
    assert(!uri && "URI handling is still far away.");
    if (!uri) {
      rc = fsl_file_canonical_name(repository, &buf, 0);
      if(rc) goto end;
      zTmp = fsl_buffer_take(&buf);
      fcli_fax(zTmp);
      repository = zTmp;
    }else if(repodir) {
      assert(!"Not handled until URI support is added");
      rc = fsl_file_canonical_name(repodir, &buf, 0);
      if(rc) goto end;
      zTmp = fsl_buffer_take(&buf);
      fcli_fax(zTmp);
      repodir = zTmp;
    }
    if(fsl_dir_check(workdir) < 1) {
      rc = fsl_mkdir_for_file(workdir, 0);
      if(rc) {
        rc = fcli_err_set(rc, "Cannot create directory [%s].", workdir);
        goto end;
      }
    }
    if ((rc = fsl_chdir(workdir))) {
      rc = fcli_err_set(rc, "Cannot chdir to [%s].",
                        workdir);
      goto end;
    }
  }

  if((rc = fsl_getcwd(cwd, FILENAME_MAX, NULL))){
    rc = fcli_err_set(rc, "Rather unexpectedly cannot get the cwd!\n");
    goto end;
  }
  if (!keep && !force) {
    rc = fsl_dir_is_empty(cwd);
    assert(rc>=0)/*"cannot" be <0 because fsl_getcwd() succeeded a
                   few nanoseconds ago.*/;
    if(rc>0){
      rc = fcli_err_set(FSL_RC_IO, "Directory [%s] is not empty\n"
                        "use the -f|--force option to override", cwd);
      goto end;
    }
  }

  opt.checkForOpenedCkout = !nested;
  opt.targetDir = cwd;
  opt.fileOverwritePolicy = keep ? FSL_OVERWRITE_NEVER : FSL_OVERWRITE_ALWAYS;
  opt.dbOverwritePolicy = false;

  if (uri) {
    /*
     * TODO: Implement clone and open when repository arg is a URI.
     */
  } 
 
  if(rc) goto end;
  if(dbRepo){
    db = dbRepo;
  }else{
    FCLI_V(("Opening repository [%s]\n", repository));
    rc = fsl_repo_open(f, repository);
    if(rc) goto end;
    db = dbRepo = fsl_cx_db_repo(f);
    assert(db && "Can't be NULL if fsl_repo_open() succeeded, but..."
           "TODO: add API which confirms that the db handle is-a repo "
           "db in terms of schema.");
  }
  rc = fsl_cx_transaction_begin(f);
  if(rc) goto end;
  assert(!fsl_cx_db_ckout(f));
  if(empty){
    if((rc = fcli_has_unused_args(false))) goto end;
  }else{
    /* Check which version to open... */
    sym = fcli_next_arg(true);
    if((rc = fcli_has_unused_args(false))) goto end;
    else if(sym){
      rc = fsl_sym_to_rid(f, sym, FSL_SATYPE_CHECKIN,
                          &rv);
      if(rc) goto end;
    }else{
      /* Try to determine which version to open... */
      char * verCheck = 0;
      char * branch = fsl_config_get_text(f, FSL_CONFDB_REPO,
                                          "main-branch", 0);
      rc = fsl_sym_to_uuid(f, branch ? branch : "trunk",
                           FSL_SATYPE_CHECKIN,
                           &verCheck, &rv);
      fsl_free(branch);
      branch = 0;
      if(rc==FSL_RC_NOT_FOUND){
        assert(!verCheck);
        rc = fsl_sym_to_uuid(f, "tip", FSL_SATYPE_CHECKIN,
                             &verCheck, &rv);
      }
      if(rc==FSL_RC_NOT_FOUND){
        // No checkins in this repo.
        fcli_err_reset();
        assert(0==rv);
        rc = 0;
        empty = true;
        sym = 0;
      }
      if(verCheck){
        fcli_fax(verCheck);
        sym = verCheck;
      }
      if(rc) goto end;
    }
  }
  if((rc = fsl_repo_open_ckout(f, &opt))){
    goto end;
  }
  if(empty){
    goto end;
  }
  if(!keep){
    prev_ckout = 0;
  } else{
    fsl_ckout_version_info(f, &prev_ckout, 0);
  }
  assert(dbRepo && dbRepo->dbh);
  if(!fsl_db_exists(dbRepo,"SELECT 1 FROM %s.event WHERE type='ci'",
                    fsl_db_role_label(FSL_DBROLE_REPO))){
    // Assume there are no checkins in this repo, so nothing for us to do.
    f_out("Repo contains no checkins, so there is nothing to check out.\n");
    goto end;
  }
  rc = fsl_sym_to_rid(f, sym, FSL_SATYPE_CHECKIN, &rv);
  if(rc){
    // f's error state should contain a description of the problem.
    if(FSL_RC_NOT_FOUND){
      f_out("No checkout found for '%s'. This is normal if the repo "
            "has no checkins, else it is an error.\n", sym);
    }
    goto end;
  }
  uint32_t missingCount = 0;
  rc = fsl_vfile_load_from_rid(f, rv, true, &missingCount);
  if(rc) goto end;
  else if(missingCount){
    rc = fcli_err_set(FSL_RC_NOT_FOUND,
                      "Database is missing content for %"PRIu32" blob(s).");
    goto end;
  }
  assert(f->ckout.rid==rv);
  if(prev_ckout == rv){
    keep = true;  /* Why rewrite to disk? If user wants this they can use -f. */
  }
  ex.keep = keep;
  ex.extracted = 0;
  ex.kept = 0;
  ex.quiet = q;
  fsl_repo_ckout_opt cOpt = fsl_repo_ckout_opt_empty;
  cOpt.checkinRid = rv;
  cOpt.callbackState = &ex;
  cOpt.callback = fsl_repo_ckout_f_my;
  //cOpt.fileOverwritePolicy = keep ? -1 : 1;
  cOpt.setMtime = mtime;
  rc = fsl_repo_ckout(f, &cOpt);
  if(rc) goto end;
  f_out("\nChecked out %d file(s) from %s [RID: %"FSL_ID_T_PFMT"] %z.\n",
        ex.extracted, sym, rv, fsl_rid_to_uuid(f, rv));
  if(ex.kept){
    f_out("%d file(s) left unchanged on disk\n", ex.kept);
  }
  if(rv){
    rc = fcli_ckout_show_info(false);
  }
  end:
  if(f && fsl_cx_transaction_level(f)){
    if(rc) fsl_cx_transaction_end(f, 1);
    else{
      rc = fsl_cx_transaction_end(f, 0);
      rc = fsl_cx_uplift_db_error2(f, db, rc);
    }
  }
  fsl_buffer_clear(&buf);
  return fcli_end_of_main(rc);
}

