/* man.c - Read system documentation
 *
 * Copyright 2019 makepost <makepost@firemail.cc>
 *
 * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/man.html

USE_MAN(NEWTOY(man, "k:M:", TOYFLAG_USR|TOYFLAG_BIN))

config MAN
  bool "man"
  default n
  help
    usage: man [-k STRING] | [SECTION] COMMAND

    Read manual page for system command.

    -k	Search short 

    Man pages are divided into 8 sections, each with an info page (man 8 info).
    1) executables, 2) syscalls, 3) library functions, 4) /dev files,
    5) file formats (ala /etc/hosts), 6) games, 7) miscelanous, 8) sysadmin

    If you don't specify a section it'll show the lowest numbered one,
    but "man 1 mkdir" and "man 2 mkdir" are different things.

    The shell builtins don't have section 1 man pages, see the "help" command.
*/

#define FOR_man
#include <toys.h>

GLOBALS(
  char *M, *k;

  char any, cell, ex, *f, k_done, *line, **sufs;
  regex_t reg;
)

static void newln()
{
  if (FLAG(k)) return;
  if (TT.any) putchar('\n');
  if (TT.any && TT.cell != 2) putchar('\n'); // gawk alias
  TT.any = TT.cell = 0;
}

static void put(char *x)
{
  while (*x && (TT.ex || *x != '\n')) TT.any = putchar(*x++);
}

// Substitute with same length or shorter.
static void s(char *x, char *y)
{
  int i = strlen(x), j = strlen(y), k, l;

  for (k = 0; TT.line[k]; k++) if (!strncmp(x, &TT.line[k], i)) {
    memmove(&TT.line[k], y, j);
    for (l = k += j; TT.line[l]; l++) TT.line[l] = TT.line[l + i - j];
    k--;
  }
}

static char start(char *x)
{
  return !strncmp(x, TT.line, strlen(x));
}

static void trim(char *x)
{
  if (start(x)) while (*x++) TT.line++;
}

static char k(char *s) {
  TT.k_done = 2;
  if (s) TT.line = s;
  return !regexec(&TT.reg, TT.k, 0, 0, 0)||!regexec(&TT.reg, TT.line, 0, 0, 0);
}

static void do_man(char **pline, long len)
{
  if (!pline) return newln();
  TT.line = *pline;

  if (FLAG(k)) {
    if (!TT.k_done && !start(".") && !start("'") && k(strstr(*pline, "- ")))
      printf("%s %s%s", TT.k, "- "+2*(TT.line!=*pline), TT.line);
    else if (!TT.k_done && start(".so") && k(basename(*pline + 4)))
      printf("%s - See %s", TT.k, TT.line);
  } else {
    s("\\fB", ""), s("\\fI", ""), s("\\fP", ""), s("\\fR", ""); // bash bold,ita
    s("\\(aq", "'"), s("\\(cq", "'"), s("\\(dq", "\""); // bash,rsync quote
    s("\\*(lq", "\""), s("\\*(rq", "\""); // gawk quote
    s("\\(bu", "*"), s("\\(bv", "|"); // bash symbol
    s("\\&", ""), s("\\f(CW", ""); // gawk,rsync fancy
    s("\\-", "-"), s("\\(", ""), s("\\^", ""), s("\\e", "\\"); // bash escape
    s("\\*(", "#"); // gawk var

    if (start(".BR")) trim(".BR "), s(" ", ""); // bash boldpunct
    if (start(".IP")) newln(), trim(".IP "); // bash list
    if (start(".IR")) trim(".IR "), s(" ", ""); // bash itapunct

    trim(".B "); // bash bold
    trim(".BI "); // gawk boldita
    trim(".FN "); // bash filename
    trim(".I "); // bash ita
    trim(".if n "); // bash nroff
    if (start(".E")) TT.ex = TT.line[2] == 'X'; // stat example
    else if (start(".PP")) newln(); // bash paragraph
    else if (start(".SM")); // bash small
    else if (start(".S")) newln(), put(TT.line + 4), newln(); // bash section
    else if (start(".so")) put("See "), put(basename(TT.line + 4)); // lastb
    else if (start(".TH")) s("\"", " "), put(TT.line + 4); // gawk,git head
    else if (start(".TP")) newln(), TT.cell = 1; // bash table
    else if (start(".") || start("\'")); // bash,git garbage
    else if (!*TT.line); // emerge
    else {
      if (TT.cell) TT.cell++;
      if (!TT.ex) put(" ");
      put(TT.line);
    }
  }
}

// Open file, decompressing if suffix known.
static int zopen(char *s)
{
  int fds[] = {-1, -1};
  char **known = TT.sufs, *suf = strrchr(s, '.');

  if ((*fds = open(s, O_RDONLY)) == -1) return -1;
  while (suf && *known && strcmp(suf, *known++));
  if (!suf || !*known) return *fds;
  sprintf(toybuf, "%czcat"+2*(suf[1]=='g'), suf[1]);
  xpopen_both((char *[]){toybuf, s, 0}, fds);
  close(fds[0]);
  return fds[1];
}

// Try opening all the possible file extensions.
int tryfile(char *section, char *name)
{
  char *s = xmprintf("%s/man%s/%s.%s.bz2", TT.M, section, name, section), **suf;
  int dotnum, fd = -1;
  size_t len = strlen(s) - 4;

  for (dotnum = 0; dotnum <= 2; dotnum += 2) {
    suf = TT.sufs;
    while ((fd == -1) && *suf) strcpy(s + len - dotnum, *suf++), fd = zopen(s);
    // Recheck suf in zopen, because for x.1.gz name here it is "".
  }
  free(s);
  return fd;
}

void man_main(void)
{
  int fd = -1;
  char **order = (char *[]) {"1", "8", "3", "2", "5", "4", "6", "7", 0};
  TT.sufs = (char *[]) {".bz2", ".gz", ".xz", "", 0};

  if (!TT.M) TT.M = getenv("MANPATH");
  if (!TT.M) TT.M = "/usr/share/man";

  if (FLAG(k)) {
    char *d, *f;
    DIR *dp;
    struct dirent *entry;
    if (regcomp(&TT.reg, TT.k, REG_ICASE|REG_NOSUB)) error_exit("bad regex");
    while (*order) {
      d = xmprintf("%s/man%s", TT.M, *order++);
      if (!(dp = opendir(d))) continue;
      while ((entry = readdir(dp))) {
        if (entry->d_name[0] == '.') continue;
        f = xmprintf("%s/%s", d, TT.k = entry->d_name);
        if (-1 != (fd = zopen(f))) {
          TT.k_done = 0;
          do_lines(fd, '\n', do_man);
          close(fd);
        }
        free(f);
      }
      closedir(dp);
      free(d);
    }
    return regfree(&TT.reg);
  }

  if (!toys.optc) error_exit("not yet");

  if (toys.optc == 1) {
    if (strchr(*toys.optargs, '/')) fd = zopen(*toys.optargs);
    else while ((fd == -1) && *order) fd = tryfile(*order++, *toys.optargs);
    if (!*order) error_exit("no %s", *toys.optargs);

  // If they specified a section, look for file in that section
  } else if (-1 == (fd = tryfile(toys.optargs[0], toys.optargs[1])))
    error_exit("section %s no %s", toys.optargs[0], toys.optargs[1]);

  do_lines(fd, '\n', do_man);
}