/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/*
  Copyright 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 holds a custom fsl_dibu implementation which
  renders its output to an ncurses PAD
*/
#include "libfossil.h"
#include <ncurses.h>
#include <panel.h>
#include <stdarg.h>
#include <locale.h>

#include "fsl-ncurses.h"

void fnc_screen_init(void){
  if(NULL!=stdscr) return;
  setlocale(LC_ALL, "")/*needed for ncurses w/ UTF8 chars*/;
  initscr();
  nonl(); keypad(stdscr,TRUE); cbreak();
  if(has_colors()){
    start_color();
    init_pair(FNColorIndex, COLOR_BLUE, COLOR_BLACK);
    init_pair(FNColorChunkSplitter, COLOR_YELLOW, COLOR_BLACK);
    init_pair(FNColorInsert, COLOR_GREEN, COLOR_BLACK);
    init_pair(FNColorDelete, COLOR_RED, COLOR_BLACK);
    init_pair(FNColorNote, COLOR_WHITE, COLOR_RED);
  }
  curs_set(0);
}

void fnc_screen_shutdown(void){
  if(NULL!=stdscr){
    endwin();
  }
}

/**
   Characters used in rendering scrollbars.
*/
static const struct NCScrollbarConfig {
  char const * vTop;
  char const * vBottom;
  char const * hLeft;
  char const * hRight;
  char const * curPos;
  char const * filler;
} NCScrollbarConfig = {
"△",//vTop
"▽",//vBottom
"◁",//hLeft
"▷",//hRight
"▣",//curPos
"░"//filler
};

/**
   Renders a vertical scrollbar on window tgt at column x, running
   from the given top row to the row (top+barHeight). The scroll
   indicator is rendered depending on the final two arguments:
   lineCount specifies the number of lines in the scrollable
   input and currentLine specifies the top-most line number of the
   currently-displayed data. e.g. a lineCount of 100 and currentLine
   of 1 puts the indicator 1% of the way down the scrollbar. Likewise,
   a lineCount of 100 and currentLine of 90 puts the indicator at the
   90% point.
 */
void fsl_nc_scrollbar_v( WINDOW * const tgt, int top, int x, 
                         int barHeight, int lineCount,
                         int currentLine ){
  int y, bottom = top + barHeight - 1,
    dotPos = top + (int)((double)currentLine / lineCount * barHeight) + 1;
  struct NCScrollbarConfig const * const conf = &NCScrollbarConfig;
  unsigned int const attr = A_REVERSE;
  wattron(tgt, attr);
  mvwprintw(tgt, top, x, "%s", conf->vTop);
  for(y = top+1; y<bottom; ++y){
    mvwprintw(tgt, y, x, "%s", conf->filler);
  }
  wattroff(tgt, attr);
  mvwprintw(tgt, dotPos>=bottom?bottom-1:dotPos, x,
            "%s", conf->curPos);
  wattron(tgt, attr);
  mvwprintw(tgt, bottom, x, "%s", conf->vBottom);
  wattroff(tgt, attr);
}


/**
   Works like fsl_nc_scrollbar_v() but renders a horizontal scrollbar
   at the given left/top coordinates of tgt.
*/
void fsl_nc_scrollbar_h( WINDOW * const tgt, int top, int left, 
                         int barWidth, int colCount,
                         int currentCol ){
  int x, right = left + barWidth - 1,
    dotPos = left + (int)((double)currentCol / colCount * barWidth) + 1;
  struct NCScrollbarConfig const * const conf = &NCScrollbarConfig;
  unsigned int const attr = A_REVERSE;
  wattron(tgt, attr);
  mvwprintw(tgt, top, left, "%s", conf->hLeft);
  for(x = left+1; x<right; ++x){
    mvwprintw(tgt, top, x, "%s", conf->filler);
  }
  wattroff(tgt, attr);
  mvwprintw(tgt, top, dotPos>=x?x-1:dotPos,
            "%s", conf->curPos);
  wattron(tgt, attr);
  mvwprintw(tgt, top, x, "%s", conf->hRight);
  wattroff(tgt, attr);
}

static const fsl_dibu_ncu fsl_dibu_ncu_empty = {
{0,0,1,0,0},{0,0},
0/*displayLines*/,
0/*displayWidth*/,
NULL/*pad*/,
{/*cursor*/0,0},
fsl_buffer_empty_m
};
#define DSTATE(VNAME) fsl_dibu_ncu * const VNAME = (fsl_dibu_ncu *)b->pimpl

#if 0
static int fdb__out(fsl_dibu *const b,
                    char const *z, fsl_size_t n){
  return b->opt->out(b->opt->outState, z, n);
}
static int fdb__outf(fsl_dibu * 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_dibu const * const b,
                       fsl_dibu_ncu 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 FDBCols_NUM1:
    case FDBCols_NUM2:
    case FDBCols_MOD: return sst->maxWidths[mwIndex];
    case FDBCols_TEXT1: case FDBCols_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(fsl_dibu_ncu * const sst,
                                    int col,
                                    char const * const z,
                                    uint32_t n){
  switch(col){
    case FDBCols_TEXT1: n+=2; break;
    default: break;
  }
  if(sst->maxWidths[col]<n){
    sst->maxWidths[col] = n;
    //n = (uint32_t)fsl_strlen_utf8(z, (fsl_int_t)n);
    //if(sst->maxWidths[col]<n) sst->maxWidths[col] = n;
  }
}

static int fdb__ncu_pline(fsl_dibu * 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 0
  if(sst->buf.used > sst->displayWidth){
    /* ^^^ achtung, counted in bytes */
    sst->buf.used = sst->displayWidth;
    sst->buf.mem[sst->buf.used] = 0;
  }
#endif
  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_dibu * const b){
  int rc = 0;
  DSTATE(sst);
  if(1==b->passNumber){
    if(1==++b->fileCount){
      assert(!sst->pad);
      *sst = fsl_dibu_ncu_empty;
    }
    if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
      sst->displayLines += 2;
      if(b->fileCount>1){
        ++sst->displayLines/*gap before 2nd+ index line*/;
      }
    }
    sst->displayLines += 2;
    return rc;
  }
  unsigned int attr = COLOR_PAIR(FNColorIndex);
  if(0==(FSL_DIFF2_NOINDEX & b->opt->diffFlags)){
    if(sst->cursor.y){/*gap before 2nd+ index line*/;
      fdb__ncu_pline(b, attr,"");
    }
    fdb__ncu_pline(b, attr,"Index #%d: %s", (int)b->fileCount,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_dibu* const b,
                                  uint32_t lnnoLHS, uint32_t linesLHS,
                                  uint32_t lnnoRHS, uint32_t linesRHS ){
  if(1==b->passNumber){
    DSTATE(sst);
#if 0
    if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
      ++sst->displayLines;
    }
#endif
    ++sst->displayLines;
    return 0;
  }
  unsigned int const attr = COLOR_PAIR(FNColorChunkSplitter);
#if 0
  if(FSL_DIFF2_LINE_NUMBERS & b->opt->diffFlags){
    fdb__ncu_pline(b,attr, "%.40c", '~');
  }
#endif
  return fdb__ncu_pline(b,attr, "@@ -%" PRIu32 ",%" PRIu32
                       " +%" PRIu32 ",%" PRIu32 " @@",
                       lnnoLHS, linesLHS, lnnoRHS, linesRHS);
}

static int fdb__ncu_skip(fsl_dibu * 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_dibu * 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_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_TEXT1, pLine->z, pLine->n);
    //fdb__dico_update_maxlen(sst, FDBCols_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_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnRHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_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(FNColorInsert)|A_BOLD,
                     "+%.*s\n",
                     (int)pLine->n, pLine->z);
  return rc;
}
static int fdb__ncu_deletion(fsl_dibu * const b, fsl_dline const * pLine){
  DSTATE(sst);
  ++b->lnLHS;
  if(1==b->passNumber){
    ++sst->displayLines;
    fdb__dico_update_maxlen(sst, FDBCols_TEXT1, 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(FNColorDelete)|A_BOLD,
                     "-%.*s\n",
                     (int)pLine->n, pLine->z);
}
static int fdb__ncu_replacement(fsl_dibu * 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_dibu * 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_dibu * const b){
  int rc = 0;
  DSTATE(sst);
  if(1==b->passNumber){
    sst->lineCount[0] = b->lnLHS;
    sst->lineCount[1] = b->lnRHS;
    if(sst->pad){
      int w, h, maxW;
      getmaxyx(sst->pad, h, w);
      if(h){/*unused but we need it for the getmaxyx() call!*/}
      maxW = (int)sst->displayWidth>=w+2
        ? (int)sst->displayWidth
        : w+2;
      if(wresize(sst->pad, sst->displayLines, maxW)){
        rc = FSL_RC_OOM;
      }
      sst->displayWidth = (unsigned)maxW;
    }else{
      sst->displayWidth = 5/*semi-magic: avoids RHS truncation*/;
      for( int i = 0; i<FDBCols_count; ++i ){
        sst->displayWidth += sst->maxWidths[i];
      }
      ++sst->displayLines/* "END" marker */;
      sst->pad = newpad((int)sst->displayLines, (int)sst->displayWidth);
      if(!sst->pad) rc = FSL_RC_OOM;
    }
    if(FSL_RC_OOM==rc){
      rc = fcli_err_set(rc, "Could not (re)allocate PAD for "
                        "diff display.");
    }else{
      fsl_buffer_reserve(&sst->buf, sst->displayWidth*2);
    }
    return rc;
  }
  return rc;
}

static int fdb__ncu_finally(fsl_dibu * const b){
  DSTATE(sst);
  return fdb__ncu_pline(b, A_REVERSE, "(END)%.*c",
                        (int)sst->displayWidth-5, '=');
}

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

bool fsl_dibu_is_ncu(fsl_dibu const * const b){
  return b && b->typeID==&fsl_dibu_ncu_empty;
}

fsl_dibu_ncu * fsl_dibu_ncu_pimpl(fsl_dibu * const b){
  return b->typeID==&fsl_dibu_ncu_empty
    ? (fsl_dibu_ncu*)b->pimpl : NULL;
}

fsl_dibu * fsl_dibu_ncu_alloc(void){
  fsl_dibu * rc =
    fsl_dibu_alloc((fsl_size_t)sizeof(fsl_dibu_ncu));
  if(rc){
    rc->typeID = &fsl_dibu_ncu_empty;
    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->finally = fdb__ncu_finally;
    rc->finalize = fsl__diff_builder_ncu_finalizer;
    rc->twoPass = true;
    assert(0!=rc->pimpl);
    fsl_dibu_ncu * const sst = (fsl_dibu_ncu*)rc->pimpl;
    *sst = fsl_dibu_ncu_empty;
    assert(0==rc->implFlags);
    assert(0==rc->lnLHS);
    assert(0==rc->lnRHS);
    assert(NULL==rc->opt);
  }
  return rc;
}

void fsl_dibu_ncu_basic_loop(fsl_dibu_ncu * const nc,
                             WINDOW * const w){
  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*/;
  bool showHeader = 1;
  if(!nc->pad){
    mvwaddstr(stdscr,1,0,"No diff output generated. Tap a key.");
    wrefresh(stdscr);
    wgetch(stdscr);
    return;
  }
  keypad(w,TRUE);
  cbreak();
  getyx(w,wTop,wLeft);
  getmaxyx(nc->pad,pH,pW);
  noecho();
  while(1){
    getmaxyx(w,wH,wW)/*can change dynamically*/;
    //touchwin(w);
    bool const scrollV = wH<pH;
    bool const scrollH = wW<pW;
    vH = wH - scrollV;
    vW  = wW - wLeft - scrollH;
    if(vH>=LINES){
      vH = LINES - scrollH;
    }
    if(vW>=COLS-1){
      vW = COLS-wLeft-scrollH;
    }
    if(pTop + wH - scrollH >= pH) pTop = pH - wH + scrollH;
    if(pTop<0) pTop=0;
    if(pLeft<0) pLeft=0;
    else if(pW - pLeft < vW) pLeft = pW - vW; 
    if(showHeader){
      unsigned int attr = COLOR_PAIR(FNColorNote) | A_BOLD | A_BLINK;
      char const * zLabel = "Arrows scroll, 'q'uits, ctrl-l refreshes.";
      wattron(w, attr);
      mvwprintw(w,1,(vW/2 - fsl_strlen(zLabel)/2), zLabel);
      wattroff(w, attr);
      showHeader = false;
    }
    prefresh(nc->pad, pTop, pLeft, wTop, wLeft, vH,vW);
    if(scrollV) fsl_nc_scrollbar_v(w, 0, wW-1, wH-1+!scrollH,
                                   pH - vH, pTop);
    if(scrollH) fsl_nc_scrollbar_h(w, wH-1, 0, wW-1+!scrollV,
                                   pW - vW, pLeft);
    doupdate();
    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;
      case KEY_ctrl('l'):
        wclear(w);
        wrefresh(w);
        break;        
    }
  }
}
