/* -*- 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 is a scratchpad for implementing a custom diff builder. It is
  not a complete app, nor does it necessarily work.
*/
#include "libfossil.h"
#include <curses.h>
#include <panel.h>
#include <stdarg.h>

/**
   Column indexes for DiffCounter::cols.
*/
enum DiffCounterCols {
DICO_NUM1 = 0, DICO_TEXT1,
DICO_MOD,
DICO_NUM2, DICO_TEXT2,
DICO_count
};
/**
   Internal state for the text-mode split diff builder. Used for
   calculating column widths in the builder's first pass so that
   the second pass can output everything in a uniform width.
*/
struct DiffNCU {
  /**
     Largest column width we've yet seen. These are only updated for
     DICO_TEXT1 and DICO_TEXT2. The others currently have fixed widths.

     FIXME: these are in bytes, not text columns. The current code may
     truncate multibyte characters.
  */
  uint32_t maxWidths[DICO_count];
  /**
     Max line numbers seen for the LHS/RHS input files. This is likely
     much higher than the number of diff lines.

     This can be used, e.g., to size and allocate a curses PAD in the
     second pass of the start() method.
  */
  uint32_t lineCount[2];
  /**
     The actual number of lines needed for rendering the file.
  */
  uint32_t displayLines;
  uint32_t displayWidth;
  WINDOW * pad;
  struct {
    int y;
    int x;
  } cursor;
  struct {
    int top;
    int left;
  } scrollPos;
  fsl_buffer buf;
};
typedef struct DiffNCU DiffNCU;
static const DiffNCU DiffNCU_empty = {
{6,10,3,6,10},{0,0},
0/*displayLines*/,
0/*displayWidth*/,
NULL/*pad*/,
{/*cursor*/0,0},
{/*scrollPos*/0,0},
fsl_buffer_empty_m,
};
#define DSTATE(VNAME) DiffNCU * const VNAME = (DiffNCU *)b->pimpl

#if 0
static int fdb__out(fsl_diff_builder *const b,
                    char const *z, fsl_size_t n){
  return b->opt->out(b->opt->outState, z, n);
}
static int fdb__outf(fsl_diff_builder * const b,
                     char const *fmt, ...){
  int rc = 0;
  if(NULL==stdscr){
    va_list va;
    assert(b->opt->out);
    va_start(va,fmt);
    rc = fsl_appendfv(b->opt->out, b->opt->outState, fmt, va);
    va_end(va);
  }
  return rc;
}
#endif

#if 0
static int maxColWidth(fsl_diff_builder const * const b,
                       DiffNCU const * const sst,
                       int mwIndex){
  static const short minColWidth =
    10/*b->opt.columnWidth values smaller than this are treated as
        this value*/;
  switch(mwIndex){
    case DICO_NUM1:
    case DICO_NUM2:
    case DICO_MOD: return sst->maxWidths[mwIndex];
    case DICO_TEXT1: case DICO_TEXT2: break;
    default:
      assert(!"internal API misuse: invalid column index.");
      break;
  }
  int const y =
    (b->opt->columnWidth>0
     && b->opt->columnWidth<=sst->maxWidths[mwIndex])
    ? (int)b->opt->columnWidth
    : (int)sst->maxWidths[mwIndex];
  return minColWidth > y ? minColWidth : y;
}
#endif

static void fdb__dico_update_maxlen(DiffNCU * const sst,
                                    int col,
                                    char const * const z,
                                    uint32_t n){
  if(sst->maxWidths[col]<n){
#if 0
    sst->maxWidths[col] = n;
#else
    n = (uint32_t)fsl_strlen_utf8(z, (fsl_int_t)n);
    if(sst->maxWidths[col]<n) sst->maxWidths[col] = n;
    if(sst->maxWidths[col] > sst->displayWidth){
      sst->displayWidth = sst->maxWidths[col];
    }
#endif
  }
}

static int fdb__ncu_pline(fsl_diff_builder * const b,
                         unsigned ncattr,
                         char const *fmt,
                         ...){
  DSTATE(sst);
  va_list args;
  va_start(args, fmt);
  fsl_buffer_appendfv(fsl_buffer_reuse(&sst->buf), fmt, args);
  if(sst->buf.used>=sst->displayWidth){
    sst->buf.used = sst->displayWidth;
    sst->buf.mem[sst->buf.used] = 0;
  }
  if(ncattr) wattron(sst->pad, ncattr);
  mvwaddstr(sst->pad, sst->cursor.y++, 0,
            fsl_buffer_cstr(&sst->buf));
  if(ncattr) wattroff(sst->pad, ncattr);
  va_end(args);
  return 0;
}

static int fdb__ncu_start(fsl_diff_builder * const b){
  int rc = 0;
  DSTATE(sst);
  if(1==b->passNumber){
    if(!b->fileCount){
      assert(!sst->pad);
      *sst = DiffNCU_empty;
      ++b->fileCount;
    }
    if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
      sst->displayLines += 2;
    }
    sst->displayLines += 2;
    return rc;
  }
  unsigned int attr = COLOR_PAIR(4);
  if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
    fdb__ncu_pline(b, attr,"Index: %s", b->opt->nameLHS/*RHS?*/);
    fdb__ncu_pline(b, attr,"%.*c", (int)sst->displayWidth, '=');
  }
  fdb__ncu_pline(b, attr,"--- %s", b->opt->nameLHS);
  fdb__ncu_pline(b, attr,"--- %s", b->opt->nameRHS);
  return rc;
}


static int fdb__ncu_chunkHeader(fsl_diff_builder* const b,
                                  uint32_t lnnoLHS, uint32_t linesLHS,
                                  uint32_t lnnoRHS, uint32_t linesRHS ){
  if(1==b->passNumber){
    DSTATE(sst);
    if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
      ++sst->displayLines;
    }
    ++sst->displayLines;
    return 0;
  }
  unsigned int const attr = COLOR_PAIR(3);
  if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
    fdb__ncu_pline(b,attr, "%.40c", '~');
  }
  return fdb__ncu_pline(b,attr, "@@ -%" PRIu32 ",%" PRIu32
                       " +%" PRIu32 ",%" PRIu32 " @@",
                       lnnoLHS, linesLHS, lnnoRHS, linesRHS);
}

static int fdb__ncu_skip(fsl_diff_builder * const b, uint32_t n){
  if(1==b->passNumber){
    b->lnLHS += n;
    b->lnRHS += n;
  }
  return 0;
}

/**
   TODO: stuff the line numbers, if enabled, in a buffer in b->pimpl, and have
   fdb__ncu_pline() prefix that if it's not empty.
*/
static int fdb__ncu_lineno(fsl_diff_builder * const b, uint32_t lnL, uint32_t lnR){
  int rc = 0;
  if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
#if 0
    rc = lnL
      ? fdb__outf(b, "%s%6" PRIu32 "%s ",
                  (lnR ? "" : b->opt->ansiColor.deletion),
                  lnL,
                  (lnR ? "" : b->opt->ansiColor.reset))
      : fdb__out(b, "       ", 7);
    if(0==rc){
      rc = lnR
      ? fdb__outf(b, "%s%6" PRIu32 "%s ",
                  (lnL ? "" : b->opt->ansiColor.insertion),
                  lnR,
                  (lnL ? "" : b->opt->ansiColor.reset))
      : fdb__out(b, "       ", 7);
    }
#endif
  }
  return rc;
}

static int fdb__ncu_common(fsl_diff_builder * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, DICO_TEXT1, pLine->z, pLine->n);
    fdb__dico_update_maxlen(sst, DICO_TEXT2, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, b->lnLHS, b->lnRHS);
  return rc ? rc : fdb__ncu_pline(b, 0, " %.*s", (int)pLine->n, pLine->z);
}
static int fdb__ncu_insertion(fsl_diff_builder * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, DICO_TEXT1, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, 0, b->lnRHS);
  return rc ? rc
    : fdb__ncu_pline(b, COLOR_PAIR(2), "+%.*s\n",
                    (int)pLine->n, pLine->z);
  return rc;
}
static int fdb__ncu_deletion(fsl_diff_builder * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, DICO_TEXT2, pLine->z, pLine->n);
    return 0;
  }
  const int rc = fdb__ncu_lineno(b, b->lnLHS, 0);
  return rc ? rc
    : fdb__ncu_pline(b, COLOR_PAIR(1), "-%.*s\n",
                    (int)pLine->n, pLine->z);
}
static int fdb__ncu_replacement(fsl_diff_builder * const b,
                                fsl_dline const * lineLhs,
                                fsl_dline const * lineRhs) {
  int rc = b->deletion(b, lineLhs);
  if(0==rc) rc = b->insertion(b, lineRhs);
  return rc;
}
                 
static int fdb__ncu_edit(fsl_diff_builder * const b,
                           fsl_dline const * lineLHS,
                           fsl_dline const * lineRHS){
  int rc = b->deletion(b, lineLHS);
  if(0==rc) rc = b->insertion(b, lineRHS);
  return rc;
}

static int fdb__ncu_finish(fsl_diff_builder * const b){
  DSTATE(sst);
  if(1==b->passNumber){
    sst->lineCount[0] = b->lnLHS;
    sst->lineCount[1] = b->lnRHS;
    if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
      sst->displayWidth += sst->maxWidths[DICO_NUM1]
        + sst->maxWidths[DICO_NUM2];
    }
    fsl_buffer_reserve(&sst->buf, sst->displayWidth*2);
    int rc = 0;
    if(sst->pad){
      int w, h, maxW;
      getmaxyx(sst->pad, h, w);
      if(h){/*unused but we need it for the getmaxyx() call!*/}
      ++sst->displayLines;
      maxW = (int)sst->displayWidth>w ? w : (int)sst->displayWidth;
      if(wresize(sst->pad, sst->displayLines+1, maxW)){
        rc = FSL_RC_OOM;
      }
    }else{
      sst->pad = newpad((int)sst->displayLines+1, (int)sst->displayWidth);
      if(!sst->pad) rc = FSL_RC_OOM;
    }
    return rc;
  }
  int rc = 0;
  rc = fdb__ncu_pline(b, A_REVERSE, "%.*c", (int)sst->displayWidth, '=');
  return rc;
}

static void fsl__diff_builder_ncu_finalizer(fsl_diff_builder * const b){
  DSTATE(sst);
  fsl_buffer_clear(&sst->buf);
  if(sst->pad){
    delwin(sst->pad);
  }
  *b = fsl_diff_builder_empty;
  fsl_free(b);
}

static fsl_diff_builder * fsl__diff_builder_ncu(void){
  fsl_diff_builder * rc =
    fsl_diff_builder_alloc((fsl_size_t)sizeof(DiffNCU));
  if(rc){
    rc->chunkHeader = fdb__ncu_chunkHeader;
    rc->start = fdb__ncu_start;
    rc->skip = fdb__ncu_skip;
    rc->common = fdb__ncu_common;
    rc->insertion = fdb__ncu_insertion;
    rc->deletion = fdb__ncu_deletion;
    rc->replacement = fdb__ncu_replacement;
    rc->edit = fdb__ncu_edit;
    rc->finish = fdb__ncu_finish;
    rc->finalize = fsl__diff_builder_ncu_finalizer;
    rc->twoPass = true;
    assert(0!=rc->pimpl);
    DiffNCU * const sst = (DiffNCU*)rc->pimpl;
    *sst = DiffNCU_empty;
    assert(0==rc->implFlags);
    assert(0==rc->lnLHS);
    assert(0==rc->lnRHS);
    assert(NULL==rc->opt);
  }
  return rc;
}



static void fcli_local_help(){
  puts("If neither --v1 nor --v2 are supplied (nor implied!) then it behaves as if "
       "it were passed \"--v1=current --v2=.\", where '.' is a symbolic "
       "name for the local checkout.\n");

  puts("All non-flag parameters, after flag processing is finished, are treated "
       "as filenames/globs and diffs are restricted to files matching those "
       "names/globs.\n");
}

static struct VDiffApp {
  char const * glob;
  short contextLines;
  short sbsWidth;
  int diffFlags;
  bool brief;
  fsl_buffer fcontent1;
  fsl_buffer fcontent2;
  fsl_buffer fhash;
  fsl_list globs;
  fsl_diff_opt diffOpt;
  fsl_diff_builder * diffBuilder;
} VDiffApp = {
NULL/*glob*/,
5/*contextLines*/,
0/*sbsWidth*/,
0/*diffFlags*/,
0/*brief*/,
fsl_buffer_empty_m/*fcontent1*/,
fsl_buffer_empty_m/*fcontent2*/,
fsl_buffer_empty_m/*fhash*/,
fsl_list_empty_m/*globs*/,
fsl_diff_opt_empty_m/*diffOpt*/,
NULL/*diffBuilder*/
};

static int f_vdiff_hash(fsl_card_F const * const fc,
                        fsl_buffer const * const content,
                        fsl_buffer * const hash){
  int rc = 0;
  fsl_buffer_reuse(hash);
  if(fc->uuid){
    rc = fc->uuid[FSL_STRLEN_SHA1]
      ? fsl_sha3sum_buffer(content, hash)
      : fsl_sha1sum_buffer(content, hash);
  }
  return rc;
}
#define f__out if(NULL==stdscr) f_out

/**
   Check two F-cards for differences and render any they have. vid1
   and vid2 are the checkin versions from which the given cards are
   from. ONE of vid1 or vid2 may be 0, indicating the current
   checkout, in which case we compare the checked-out copy of that
   file. Returns 0 on success, non-0 on fatal error.
*/
int f_vdiff_files(fsl_cx * f,
                  fsl_id_t vid1,
                  fsl_card_F const * fc1,
                  fsl_id_t vid2,
                  fsl_card_F const * fc2){
  int rc = 0;
  fsl_buffer * fContent1 = &VDiffApp.fcontent1;
  fsl_buffer * fContent2 = &VDiffApp.fcontent2;
  fsl_buffer * fLocalHash = &VDiffApp.fhash
    /* hash of the local checked-out file */;
  fsl_time_t rmtime = 0;
  fsl_time_t fmtime = 0;
  fsl_card_F const * fcHashCmp = NULL
    /* The card against which we will compare fLocalHash.  If
       fLocalHash is the hash of vid1 then this will be fc2, and vice
       versa. */;
  if(vid1>0 && vid2>0 && !fsl_uuidcmp(fc1->uuid, fc2->uuid)){
    /* No diffs to check */
    return 0;
  }

  /**
     TODO: optimization: use vfile where we can to reduce the
     set of files we scan. This can only work if one version
     is the checked-out version and one is the local checkout
     changes.
  */

  /* Else different content in each version OR we have a local file
     and need to load it to see if it's changed. */
  fLocalHash->used = fContent2->used = fContent1->used = 0;

  assert(vid1!=vid2);

  if(0==vid1){ /* vid1 is the current checkout version */
    assert(0 != vid2);
    rc = fsl_ckout_file_content(f, 0, fc1->name, fContent1);
    if(!rc){
      rc = f_vdiff_hash(fc1, fContent1, fLocalHash);
      if(!rc){
        fcHashCmp = fc2;
        rc = fsl_card_F_ckout_mtime(f, vid1, fc1, NULL, &fmtime);
      }
    }
  }else{
    rc = fsl_card_F_content(f, fc1, fContent1);
    if(!rc && (0==vid2)){
      /* Collect the repo-side mtime IF the other version==0. */
      rc = fsl_card_F_ckout_mtime(f, vid1, fc1, &rmtime, NULL);
    }
  }

  if(rc) return rc;

  /* Repeat for vid2. */
  if(0==vid2){ /* vid2 is the current checkout */
    assert(0 != vid1);
    rc = fsl_ckout_file_content(f, 0, fc2->name, fContent2);
    if(!rc){
      rc = f_vdiff_hash(fc2, fContent2, fLocalHash);
      if(!rc){
        fcHashCmp = fc1;
        rc = fsl_card_F_ckout_mtime(f, vid2, fc2, NULL, &fmtime);
      }
    }
  }else{
    rc = fsl_card_F_content(f, fc2, fContent2);
    if(!rc && (0==vid1)){
      /* Collect the repo-side mtime IF the other version==0. */
      rc = fsl_card_F_ckout_mtime(f, vid2, fc2, &rmtime, NULL);
    }
  }

  if(rc) return rc;
  else if(fcHashCmp
          && (0==fsl_uuidcmp(fsl_buffer_cstr(fLocalHash),
                             fcHashCmp->uuid))
          ){
    /* repo-side content is unchanged from local copy. */
    return 0;
  }else if((fmtime>0) && (fmtime==rmtime)){
    /* One of the above is a local file and rmtime holds the repo-side
       mtime of the other. Assume naively that same time==same
       content, as that will be the case more often then not.
    */
    return 0;
  }else{
    char const * zUuid1 = (0==vid1) ? "checkout" : fc1->uuid;
    char const * zUuid2 = (0==vid2) ? "checkout" : fc2->uuid;
    if(VDiffApp.brief){
      f__out("DIFF: %.8s ==> %.8s %s\n", zUuid1, zUuid2,
             fc2->name);
    }else{
      VDiffApp.diffOpt.hashLHS = zUuid1;
      VDiffApp.diffOpt.hashRHS = zUuid2;
      VDiffApp.diffOpt.nameLHS = fc1->name;
      VDiffApp.diffOpt.nameRHS = fc2->name;
      assert(VDiffApp.diffBuilder->opt == &VDiffApp.diffOpt);
      rc = fsl_diff_v2(fContent1, fContent2, VDiffApp.diffBuilder);
      if(rc){
        fcli_err_set(rc, "Error %s generating diff.", fsl_rc_cstr(rc));
      }else{
        f__out("\n") /* only for compat with fossil(1) */;
      }
    }
    return rc;
  }
}

/**
   Outputs a diff of the two given checkin version RIDs. v1 is, for
   purposes of this algorithm, considered to be the older of the two.

   ONE of the versions may be 0 to indicate the current local
   checkout, which differs semantically from the checked-out version
   in that a version of 0 causes local copies of those files of be
   diffed instead of the checked-in version (of the checked-out
   version! (got that?)).

   That is, v1 of 0 and v1 of 999 might refer to the same checkin
   version, but 0 will cause diffs to be calculated based on
   the local checkout copies, whereas 999 will use the copies
   from the database.

   It sends all output to f_out() and takes its diff-level
   configuration from the VDiffApp global.
*/
static int f_vdiff(fsl_id_t v1, fsl_id_t v2){
  int rc = 0;
  fsl_deck d1 = fsl_deck_empty;
  fsl_deck d2 = fsl_deck_empty;
  fsl_cx * f = fcli_cx();
  fsl_card_F const * fc1 = NULL;
  fsl_card_F const * fc2 = NULL;
  int nameCmp = 0;
  fsl_buffer c1 = fsl_buffer_empty;
  fsl_buffer c2 = fsl_buffer_empty;

  rc = fsl_deck_load_rid(f, &d1, v1, FSL_SATYPE_CHECKIN);
  if(rc) goto end;
  rc = fsl_deck_load_rid(f, &d2, v2, FSL_SATYPE_CHECKIN);
  if(rc) goto end;
  rc = fsl_deck_F_rewind(&d1);
  if(!rc) rc = fsl_deck_F_rewind(&d2);
  if(rc) goto end;

  /*
    Reminder: if v1==0 or v2==0, we need slightly different semantics.
    fsl_deck_load_rid() equates 0 to the current checkout, which is
    half right.  We actually want the content of the current local
    checkout for that case.

    TODO: optimization: if v1==checkout version and v2==local changes,
    filter our result set based on vfile entries which have marked
    changes. We will need fsl_vfile_changes_scan() for that, which is
    current marked internal but should be moved into the public
    API anyway.
  */

#define GLOBMATCH(FC) (!VDiffApp.globs.used ? 1 : !!fsl_glob_list_matches(&VDiffApp.globs, (FC)->name))

  fsl_deck_F_next(&d1, &fc1);
  fsl_deck_F_next(&d2, &fc2);
  while(fc1 || fc2){
    if(!fc1) nameCmp = 1;
    else if(!fc2) nameCmp = -1;
    else{
      char const * zNameToCmp = fc2->priorName ? fc2->priorName : fc2->name;
      nameCmp = fsl_strcmp(fc1->name, zNameToCmp);
    }
    if(nameCmp<0){
      assert(fc1);
      if(GLOBMATCH(fc1)){
        f__out("REMOVED: %s\n", fc1->name);
      }
      fsl_deck_F_next(&d1, &fc1);
    }else if(nameCmp>0){
      if(GLOBMATCH(fc2)){
        f__out("ADDED: %s\n", fc2->name);
      }
      fsl_deck_F_next(&d2, &fc2);
    }else if(v1 && v2 && 0==fsl_strcmp(fc1->uuid, fc2->uuid)){
      fsl_deck_F_next(&d1, &fc1);
      fsl_deck_F_next(&d2, &fc2);
    }else{
      if(GLOBMATCH(fc2)){
        rc = f_vdiff_files(f, v1, fc1, v2, fc2);
        if(rc) goto end;
      }
      fsl_deck_F_next(&d1, &fc1);
      fsl_deck_F_next(&d2, &fc2);
    }
  }/*while(f-cards)*/;

#undef GLOBMATCH
  end:
  fsl_deck_finalize(&d1);
  fsl_deck_finalize(&d2);
  fsl_buffer_clear(&c1);
  fsl_buffer_clear(&c2);
  return rc;
}

static void fdb__ncu_scroll_loop(DiffNCU * const nc){
  int pTop = 0, pLeft = 0;
  int wW/*window width*/, wH/*window height*/,
    wTop/*window Y*/, wLeft/*window X*/,
    pW/*pad width*/, pH/*pad height*/,
    vH/*viewport height*/, vW/*viewport width*/,
    ch/*wgetch() result*/;
  int const hScroll = 2/*cols to shift for horizontal scroll*/;
  WINDOW * const w = stdscr/*TODO custom target*/;
  keypad(w,TRUE);
  cbreak();
  getyx(w,wTop,wLeft);
  getmaxyx(nc->pad,pH,pW);
  noecho();
  unsigned int attr = A_REVERSE | A_UNDERLINE;
  while(1){
    getmaxyx(w,wH,wW)/*can change dynamically*/;
    wattron(w, attr);
    mvwprintw(w,0,0,"Arrows scroll, 'q' quits. wH=%d wW=%d pH=%d pTop=%d dL=%u",
              wH, wW,pH, pTop, nc->displayLines);
    wclrtoeol(w);
    vH = wH-1;
    vW  = wW - wLeft;
    if(vH>=LINES){
      vH = LINES-1;
    }
    if(vW>=COLS-1){
      vW = COLS-wLeft-1;
    }
    wattroff(w, attr);
    prefresh(nc->pad, pTop, pLeft, wTop+1, wLeft, vH,vW);
    wrefresh(w);
    ch = wgetch(w);
    if('q'==ch) break;
    switch(ch){
      case KEY_HOME: pTop = 0; break;
      case KEY_END: pTop = pH - vH;
      case KEY_DOWN: ++pTop; break;
      case KEY_UP: --pTop; break;
      case KEY_NPAGE: pTop += vH; break;
      case KEY_PPAGE: pTop -= vH; break;
      case KEY_LEFT: pLeft -= hScroll; break;
      case KEY_RIGHT: pLeft += hScroll; break;
    }
    if(pTop + vH > pH) pTop = pH - vH;
    if(pTop<0) pTop=0;
    if(pLeft<0) pLeft=0;
    else if(pW - pLeft < vW) pLeft = pW - vW; 
  }
}


int main(int argc, char const * const * argv ){
  int rc = 0;
  const char * vFrom = NULL;
  const char * vTo = NULL;
  const char * glob = NULL;
  int32_t nContext = -1;
  bool flagSbs = false;
  bool flagInvert = false;
  bool flagDebug = false;
  bool flagLineNo = false;
  bool flagBW = false;
  bool flagColor = false;
  bool flagIgnoreSpaces = false;
  int flagSbsWidth = -1;
  fsl_cx * f;
  fsl_id_t idFrom = -1, idTo = -1;
  char const * checkoutAlias = ".";
  fsl_buffer globBuf = fsl_buffer_empty;

  fcli_cliflag cliFlags[] = {
    FCLI_FLAG("v1", "from", "version", &vFrom,
              "Version to diff from. May also be provided as "
              "the first non-flag argument"),
    FCLI_FLAG("v2", "to", "version", &vTo,
              "Version to diff to. May also be provided as "
              "the second non-flag argument"),
    FCLI_FLAG_BOOL("w","ignore-all-space",&flagIgnoreSpaces,
                   "Ignore all whitespace differences."),
    FCLI_FLAG_BOOL("y", "sbs", &flagSbs,
                   "Use side-by-side diff."),
    FCLI_FLAG_I32("W","sbs-width","max column width",&flagSbsWidth,
                  "Max side-by-side diff view width. Implies -y."),
    FCLI_FLAG_I32("c", "context", "integer", &nContext,
              "Number of context lines."),
    FCLI_FLAG("g", "glob", "string", &glob,
              "Lists only changes to filenames matching the given "
              "comma-separated globs. May be passed multiple times. "
              "All non-flag arguments after the versions are treated "
              "as globs."),
    FCLI_FLAG_BOOL("l","line-numbers",&flagLineNo,
                   "Add line numbers to unified diff output."),
    FCLI_FLAG_BOOL("i","invert",&flagInvert,
                   "Invert the direction of the diff."),
    FCLI_FLAG_BOOL("b","brief", &VDiffApp.brief,
                   "Elides actual diffs and only summarizes "
                   "the changes."),
    FCLI_FLAG_BOOL("bw", "no-color", &flagBW,
                   "Disable color output. This is automatic if stdout "
                   "is not a tty."),
    FCLI_FLAG_BOOL(NULL,"color", &flagColor,
                   "Try to force ANSI color even if stdout is not "
                   "a terminal or --no-color is used. Does not work "
                   "with all diff formats."),
    FCLI_FLAG_BOOL(NULL,"debug", &flagDebug,
                   "Use the debug/test diff builder."),
    fcli_cliflag_empty_m
  };
  fcli_help_info FCliHelp = {
    "Generate diffs of different repository and/or the "
    "local checkout versions.",
    "[filenames or quoted globs...]",
    fcli_local_help
  };
  assert(NULL==stdscr);
  rc = fcli_setup_v2(argc, argv, cliFlags, &FCliHelp);
  if(rc) goto end;

  f = fcli_cx();
  if(!fsl_cx_db_repo(f)){
    rc = fcli_err_set(FSL_RC_NOT_A_REPO,
                      "Requires a repository db. See --help.");
    goto end;
  }

  while(glob){
    fsl_glob_list_parse(&VDiffApp.globs, glob);
    glob = NULL;
    fcli_flag2("g","glob", &glob);
  }

  if(fcli_has_unused_flags(0)) goto end;

  if(flagIgnoreSpaces) VDiffApp.diffOpt.diffFlags |= FSL_DIFF2_IGNORE_ALLWS;
  if(flagInvert) VDiffApp.diffOpt.diffFlags |= FSL_DIFF2_INVERT;
  if(nContext>=0){
    VDiffApp.diffOpt.contextLines = (unsigned short)nContext;
    if(0==nContext){
      VDiffApp.diffOpt.diffFlags |= FSL_DIFF2_CONTEXT_ZERO;
    }
  }
  if(flagLineNo) VDiffApp.diffOpt.diffFlags |= FSL_DIFF2_LINE_NUMBERS;
  if(!vFrom) vFrom = fcli_next_arg(1);
  if(!vTo) vTo = fcli_next_arg(1);
  if(!vFrom && !vTo){
    /* Special case: compare current checkout repo version vs local copy. */
    vFrom = "current";
    idTo = 0;
    vTo = ".";
  }else if(vFrom && !vTo){
    /* Special case: permit (".") by itself as an alias for ("current" ".") */
    if(0==fsl_strcmp(vFrom,checkoutAlias)){
      vFrom = "current";
    }
    vTo = ".";
    idTo = 0;
  }else if(vFrom && vTo
           && 0==fsl_strcmp(vFrom,checkoutAlias)
           && 0==fsl_strcmp(vTo,checkoutAlias)){
    /* Special case: permit ("." ".") as an alias for ("current" ".") */
    vFrom = "current";
    idTo = 0;
  }else if(vTo && !vFrom){
    rc = fcli_err_set(FSL_RC_MISUSE, "Both of -v1 UUID and -v2 UUID are required.");
    goto end;
  }

  if(0==fsl_strcmp(vFrom, checkoutAlias)) idFrom = 0;
  else rc = fsl_sym_to_rid(f, vFrom, FSL_SATYPE_CHECKIN, &idFrom);
  if(!rc && idTo<0){
    if(0==fsl_strcmp(vTo, checkoutAlias)) idTo = 0;
    else rc = fsl_sym_to_rid(f, vTo, FSL_SATYPE_CHECKIN, &idTo);
  }
  if(rc) goto end;
  else if(idFrom==idTo){
    rc = fcli_err_set(FSL_RC_RANGE,
                      "Cowardly refusing to diff a version "
                      "against itself.");
    goto end;
  }

  while((glob = fcli_next_arg(true))){
    if(fsl_cx_has_ckout(f)){
      /* Check if each each entry looks like the name of an existing
         file. If so, add the repo-relative canonicalized name to
         the glob list instead of the literal glob argument. The end
         effect is that we accept filenames as well as globs. */
      fsl_buffer_reuse(&globBuf);
      if(fsl_ckout_filename_check(f, true, glob, &globBuf)){
        fcli_err_reset();
      }else{
        char const * z = fsl_buffer_cstr(&globBuf);
        if(fsl_cx_stat(f, false, z, NULL)){
          fcli_err_reset();
        }else{
          glob = z;
        }
      }
    }
    fsl_glob_list_parse(&VDiffApp.globs, glob);
  }

  if(!idTo || !idFrom){
    if(!fsl_cx_db_ckout(f)){
      rc = fcli_err_set(FSL_RC_NOT_A_CKOUT,
                        "Using the '.' (local checkout) version "
                        "alias requires a checkout.");
      goto end;
    }
  }
  if(flagSbsWidth>0){
    VDiffApp.diffOpt.columnWidth = (unsigned short)flagSbsWidth;
    flagSbs = true;
  }
  VDiffApp.diffBuilder = fsl__diff_builder_ncu();
  if(rc) goto end;
  VDiffApp.diffBuilder->opt = &VDiffApp.diffOpt;
  VDiffApp.diffOpt.out = fsl_output_f_FILE;
  VDiffApp.diffOpt.outState = stdout;
  assert(idFrom>=0);
  assert(idTo>=0);
  //f_out("vFrom=%d %s, vTo=%d %s\n", (int)idFrom, vFrom, (int)idTo, vTo);
  initscr();
  nonl(); keypad(stdscr,TRUE); cbreak();
  if(has_colors()){
    start_color();
    init_pair(1, COLOR_RED,     COLOR_BLACK);
    init_pair(2, COLOR_GREEN,   COLOR_BLACK);
    init_pair(3, COLOR_YELLOW,  COLOR_BLACK);
    init_pair(4, COLOR_BLUE,    COLOR_BLACK);
    init_pair(5, COLOR_CYAN,    COLOR_BLACK);
    init_pair(6, COLOR_MAGENTA, COLOR_BLACK);
    init_pair(7, COLOR_WHITE,   COLOR_BLACK);
  }
  curs_set(0);
  mvwaddstr(stdscr,1,0,"Running diff... please wait...");
  wrefresh(stdscr);
  rc = f_vdiff( idFrom, idTo );
  clear();
  wrefresh(stdscr);
  if(0==rc){
    DiffNCU * const sst = (DiffNCU *)VDiffApp.diffBuilder->pimpl;
    if(sst->pad){
      assert(sst->pad);
      //prefresh(sst->pad, 0, 0, 0, 0, LINES-1, COLS);
      fdb__ncu_scroll_loop(sst);
    }else{
      mvwaddstr(stdscr,1,0,"No diff output generated. Tap a key.");
      wrefresh(stdscr);
      wgetch(stdscr);
    }
  }
  end:
  fsl_glob_list_clear(&VDiffApp.globs);
  fsl_buffer_clear(&VDiffApp.fcontent1);
  fsl_buffer_clear(&VDiffApp.fcontent2);
  fsl_buffer_clear(&VDiffApp.fhash);
  fsl_buffer_clear(&globBuf);
  if(VDiffApp.diffBuilder){
    VDiffApp.diffBuilder->finalize(VDiffApp.diffBuilder);
  }
  if(NULL!=stdscr){
    endwin();
  }
  return fcli_end_of_main(rc);
}
