#include <errno.h>
#include <stddef.h>
#include <string.h>
#include <mb.h>

#define MB_KEY_DEF(str) str, sizeof(str) - 1

static char *me;
static mb_wchar_t linebuf[BUFSIZ];
static mb_wchar_t *lb = linebuf;
static mb_wchar_t *le = linebuf;
static mb_wchar_t *l = NULL;
static size_t size = 0;
static size_t end = 0;
static char *mbl = NULL;
static size_t mbl_size = 0;
static FILE *istream;
static mb_info_t *info_r;
static int istream_eof;
static FILE *ostream;
static mb_info_t *info_w;
static long nline = 1;

#define CONV_MAX (256)
static mb_ws_conv_t converterv[CONV_MAX] = {NULL};

static mb_setup_t regex_mb_setup;
static mb_info_t regex_mb_info;

static struct regex_match_st {
  mb_wchar_t *cur;
  struct {
    mb_wchar_t *beg, *end;
  } v[10];
} regex_match;

static void
regex_record_beg(uirx_wc_t i, void *arg)
{
  if (i < sizeof(regex_match.v) / sizeof(regex_match.v[0]))
    ((struct regex_match_st *)arg)->v[i].beg = ((struct regex_match_st *)arg)->cur;
}

static void
regex_record_end(uirx_wc_t i, void *arg)
{
  if (i < sizeof(regex_match.v) / sizeof(regex_match.v[0]))
    ((struct regex_match_st *)arg)->v[i].end = ((struct regex_match_st *)arg)->cur;
}

static uirx_wc_t
regex_read_alpha(void *arg)
{
  mb_wchar_t wc;

  if ((wc = mb_fetch_wchar(arg)) == mb_notchar_eof)
    wc = '\0';
  else {
    if (converterv[0])
      mb_apply_convv(&wc, &wc + 1, converterv, info_w);

    mb_conv_for_ces(&wc, &wc + 1, info_w);
  }

  return wc;
}

static void
regex_alpha_tolower(mb_wchar_t *ws, mb_wchar_t *ws_end, mb_info_t *info)
{
  for (; ws < ws_end ; ++ws)
    if (!(*ws & ~0x7F) && isupper(*ws))
      *ws = tolower(*ws);
}

static uirx_wc_t
regex_ci_alpha_filter(uirx_wc_t wc, wcrx_parser_desc_t *desc)
{
  regex_alpha_tolower(&wc, &wc + 1, info_w);
  return wc;
}

static void
regex_evprintf(void *arg, const char *frmt, va_list ap)
{
  vfprintf(stderr, frmt, ap);
}

static uirx_nfa_t *regex_nfa;

static wcrx_parser_desc_core_t regex_desc_core = {
  regex_read_alpha,
  NULL, NULL,
  mb_notchar_bof, '\0',
  regex_record_beg, regex_record_end,
  regex_evprintf,
  1,
};

static wcrx_parser_desc_t regex_desc = {
  &regex_desc_core,
  &regex_mb_info,
  NULL,
  0,
  NULL,
  NULL,
};

static wcrx_parser_desc_t regex_ci_desc = {
  &regex_desc_core,
  &regex_mb_info,
  regex_ci_alpha_filter,
  0,
  NULL,
  NULL,
};

static void
eexit(int status, const char *f, ...)
{
  va_list ap;

  va_start(ap, f);
  vfprintf(stderr, f, ap);
  va_end(ap);
  exit(status);
}

static void
storewcs(mb_wchar_t *b, mb_wchar_t *e)
{
  size_t nend, i;

  if ((nend = end + e - b) >= size) {
    size_t nsize;

    nsize = (nend / 2 + 1) * 3;

    if (!(l = realloc(l, nsize * sizeof(mb_wchar_t))))
      eexit(1, "realloc(l, %lu): %s\n", (unsigned long)(nsize * sizeof(mb_wchar_t)), strerror(errno));

    size = nsize;
  }

  for (i = end ; b < e ;)
    l[i++] = *b++;

  if (converterv[0])
    mb_apply_convv(l + end, l + nend, converterv, info_w);

  mb_conv_for_ces(l + end, l + nend, info_w);
  end = nend;
}

static mb_wchar_t *
gets_unlimited(mb_wchar_t *copy, mb_wchar_t *copy_end, size_t *p_eol)
{
  mb_wchar_t wc, *p;
  void *to;

  end = 0;

  if (copy)
    storewcs(copy, copy_end);

  for (;;) {
    if (lb < le) {
      for (p = lb ; p < le ;)
	if (*p++ == '\n') {
	  storewcs(lb, p);
	  lb = p;
	  goto end;
	}

      if (istream_eof)
	break;

      storewcs(lb, le);
    }
    else if (istream_eof)
      break;

    to = lb = le = linebuf;

    if ((wc = mb_cs_detect_encode(info_r,
				  MB_ENCODE_TO_WS | MB_ENCODE_SKIP_INVALID | MB_ENCODE_SKIP_SHORT,
				  &to, linebuf + BUFSIZ))
	== mb_notchar_eof)
      istream_eof = 1;

    le = to;
  }

  storewcs(lb, le);
  lb = le = linebuf;
end:
  if (p_eol)
    *p_eol = end;

  l[end++] = '\0';
  return l;
}

static void
chomp(mb_wchar_t *line, size_t *eol)
{
  if (*eol >= 1 && line[*eol - 1] == '\n') {
    if (*eol >= 2 && line[*eol - 2] == '\r')
      *eol -= 2;
    else
      --(*eol);

    line[*eol] = '\0';
  }
}

static char *
getmbs_unlimited(size_t *p_eol)
{
  char *e;
  mb_wchar_t *w;

  gets_unlimited(NULL, NULL, NULL);

  if (end * MB_LEN_MAX >= mbl_size) {
    size_t nsize;

    nsize = (end * MB_LEN_MAX / 2 + 1) * 3;

    if (!(mbl = realloc(mbl, nsize)))
      eexit(1, "realloc(mbl, %lu): %s\n", (unsigned long)nsize, strerror(errno));

    mbl_size = nsize;
  }

  for (e = mbl, w = l ; *w ;)
    e += mb_wchar_to_mbc(*w++, e);

  *e = '\0';

  if (p_eol)
    *p_eol = e - mbl;

  return mbl;
}

static int
mb_nc_memcmp(const char *a, size_t alen, const char *b, size_t blen)
{
  int cmp;

  if (alen < blen)
    cmp = -1;
  else if (alen > blen)
    cmp = 1;
  else {
    size_t i;

    for (cmp = 0, i = 0 ; i < alen ; ++i)
      if ((cmp = tolower((unsigned char)a[i]) - tolower((unsigned char)b[i])))
	break;
  }

  return cmp;
}

static void *
mb_nc_bsearch(const char *key, size_t keylen, void *tab,
	      size_t key_off, size_t keylen_off, size_t elem_size, size_t tab_size)
{
  size_t b, e, i;
  char *elem;
  int cmp;

  for (b = 0, e = tab_size / elem_size ; b < e ;) {
    i = (b + e) / 2;
    elem = (char *)tab + elem_size * i;

    if (!(cmp = mb_nc_memcmp(key, keylen, *(const char **)(elem + key_off), *(size_t *)(elem + keylen_off))))
      return (void *)elem;
    else if (cmp < 0)
      e = i;
    else
      b = i + 1;
  }

  return NULL;
}

struct opt_st {
  mb_setup_t isetup, osetup, *setup;
  mb_cs_detector_stat_t cs_stat[MB_CS_DETECT_CHOICEMAX];
  size_t n_cs_stats;
  const char *format, *mime_title, *cname, *regex, *regex_cs;
  int lineno, iconv, oconv, which, width, cprop, regex_ci;
};

static void
opt_regex(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  char *cs_end;

  if (*arg1st == '*' && (cs_end = strchr(&arg1st[1], '*'))) {
    cs_end[0] = '\0';

    if (arg1st[1] == '!') {
      p_opt->regex_ci = 1;

      if (&arg1st[2] == cs_end)
	p_opt->regex_cs = "utf-8";
      else
	p_opt->regex_cs = &arg1st[2];
    }
    else {
      p_opt->regex_ci = 0;
      p_opt->regex_cs = &arg1st[1];
    }

    p_opt->regex = &cs_end[1];
  }
  else {
    p_opt->regex_cs = "utf-8";
    p_opt->regex = arg1st;
  }
}

static void
opt_input(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->setup = &p_opt->isetup;
  p_opt->iconv = 1;
}

static void
opt_output(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->setup = &p_opt->osetup;
  p_opt->oconv = 1;
}

static void
opt_lineno(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->lineno = 1;
}

static void
opt_tofile(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  if (!strcmp(arg1st, "-"))
    ostream = stdout;
  else if (!*arg1st)
    ostream = NULL;
  else if (!(ostream = fopen(arg1st, "w")))
    eexit(1, "%s: fopen(\"%s\", \"w\"): %s\n", me, arg1st, strerror(errno));
}

static void
opt_appendtofile(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  if (!strcmp(arg1st, "-"))
    ostream = stdout;
  else if (!*arg1st)
    ostream = NULL;
  else if (!(ostream = fopen(arg1st, "a")))
    eexit(1, "%s: fopen(\"%s\", \"a\"): %s\n", me, arg1st, strerror(errno));
}

static void
opt_which(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->which = 1;
}

static void
opt_width(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->width = 1;
}

static void
opt_cprop(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->cprop = 1;
}

static void
opt_charset(struct opt_st *p_opt, char *s, int argrestc, char **argrestv)
{
  if (!mb_lang_to_detector(s, p_opt->cs_stat, &p_opt->n_cs_stats))
    mb_setsetup(p_opt->setup, "@", s);
}

static void
opt_mimetitle(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->mime_title = arg1st;
}

static void
opt_format(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->format = arg1st;
}

static void
opt_cname(struct opt_st *p_opt, char *arg1st, int argrestc, char **argrestv)
{
  p_opt->cname = arg1st;
}

static void
opt_flag_unknown(const char *s, size_t n)
{
  eexit(1, "%s: %.*s: unknown flag\n", me, (int)n, s);
}

static void
opt_flag(struct opt_st *p_opt, char *s, int argrestc, char **argrestv)
{
  char op[2] = "|";

  switch ((unsigned char)*s) {
  case '+':
    op[0] = '|';
    ++s;
    break;
  case '-':
  case '=':
    op[0] = *s++;
  default:
    break;
  }

  mb_setsetup(p_opt->setup, op, mb_namev_to_flag(s, 0, opt_flag_unknown));
}

static void
opt_converter_unknown(const char *s, size_t n)
{
  eexit(1, "%s: %.*s: unknown converter\n", me, (int)n, s);
}

static void
opt_convertto(struct opt_st *p_opt, char *s, int argrestc, char **argrestv)
{
  mb_namev_to_converterv(s, converterv, CONV_MAX, opt_converter_unknown);
}

static void opt_help(struct opt_st *p_opt, char *s, int argrestc, char **argrestv);

void
doit(struct opt_st *opt, const char *fn, int real_argc)
{
  int notstdin;
  mb_wchar_t *line;

  if ((notstdin = (fn && strcmp(fn, "-")) ? 1 : 0)) {
    if (!(istream = fopen(fn, "r")))
      eexit(1, "%s: fopen(%s, \"r\"): %s\n", me, fn, strerror(errno));
  }
  else
    istream = stdin;

  if (!opt->iconv)
    mb_setsetup(&opt->isetup, "@", "x-moe-internal");

  mb_fbind(istream, "r!", &opt->isetup);

  if (!mb_finfo(istream, &info_r, NULL))
    eexit(1, "%s: mb_fbind(istream, ...): %s\n", me, strerror(errno));

  istream_eof = 0;

  if (opt->n_cs_stats) {
    mb_cs_detector_t *p = mb_falloc_cs_detector(istream, BUFSIZ, 0);

    if (!p)
      eexit(1, "%s: fail to allocate detector\n", me);

    memcpy(p->stat, opt->cs_stat, sizeof(mb_cs_detector_stat_t) * opt->n_cs_stats);
    p->nstats = opt->n_cs_stats;
    p->limit = BUFSIZ * 10;
    p->flag = MB_CS_DETECT_FLAG_MKUNBOUND;
  }

  if (opt->lineno && !opt->format)
    opt->format = "#%ld: %ws";

  if (opt->width) {
    size_t eol;

    while (*(line = gets_unlimited(NULL, NULL, &eol))) {
      chomp(line, &eol);
      fprintf(stderr, "%ld\n", (long)mb_wstr_width(line));
    }
  }
  else if (opt->cprop) {
    size_t eol;

    while (*(line = gets_unlimited(NULL, NULL, &eol))) {
      mb_wchar_t *s;
      char propbuf[] = "S)(BN>\n";

      chomp(line, &eol);

      for (s = line ; *s ;) {
	mb_wchar_t wc;
	int prop, set, fc, c;
	size_t pe = 0;

	wc = *s++;
	prop = mb_wchar_prop(wc);
	c = MB_WORD_DEC(wc, set, fc);

	if (prop & MB_CPROP_NEVER_EOL) propbuf[pe++] = '(';
	if (prop & MB_CPROP_IS_SPACE) propbuf[pe++] = 'S';
	if (prop & MB_CPROP_MAY_BREAK) propbuf[pe++] = 'B';
	if (prop & MB_CPROP_EOL_TO_NULL) propbuf[pe++] = 'N';
	if (prop & MB_CPROP_NEVER_BOL) propbuf[pe++] = ')';
	propbuf[pe++] = '>';
	propbuf[pe++] = *s ? ' ' : '\n';
	propbuf[pe++] = '\0';
	fprintf(stderr, "<cc=0x%04X, set=%d, fc=0x%02X, prop=%s", c, set, fc, propbuf);
      }
    }
  }
  else if (opt->regex || opt->format)
    for (;; ++nline) {
      size_t eol;
      static mb_wchar_t bol[1] = {mb_notchar_bof};

      line = gets_unlimited(bol, bol + 1, &eol);

      if (!line[1])
	break;

      chomp(line, &eol);

      if (ostream) {
	if (opt->regex) {
	  ptrdiff_t i;
	  mb_wchar_t *next;

	  regex_match.cur = line;

	  if (converterv[0])
	    mb_apply_convv(line, line + eol, converterv, info_w);

	  mb_conv_for_ces(line, line + eol, info_w);

	  if (opt->regex_ci)
	    regex_alpha_tolower(line, line + eol, info_w);

	  while (regex_match.cur <= line + eol) {
	    for (i = 0 ; i < sizeof(regex_match.v) / sizeof(regex_match.v[0]) ; ++i)
	      regex_match.v[i].beg = regex_match.v[i].end = NULL;

	    uirx_match_start(regex_nfa);

	    if (uirx_match(regex_nfa, &regex_match, *regex_match.cur)) {
	      for (next = ++(regex_match.cur) ; regex_match.cur <= line + eol ; ++(regex_match.cur))
		if (!uirx_match(regex_nfa, &regex_match, *regex_match.cur))
		  goto try_match;

	      uirx_match_end(regex_nfa, &regex_match);
	    try_match:
	      if (regex_match.v[0].beg && regex_match.v[0].end)
		goto match_found;

	      regex_match.cur = next;
	      continue;
	    }

	    ++(regex_match.cur);
	  }

	  continue;
	match_found:
	  ;
	}
	else if (!*line)
	  break;

	if (real_argc > 1)
	  mb_fprintf(ostream, "%s: ", fn ? fn : "-");

	if (opt->lineno)
	  mb_fprintf(ostream, opt->format, nline, line);
	else if (opt->format)
	  mb_fprintf(ostream, opt->format, line);
	else {
	  mb_decode(line, line + eol, info_w);
	  mb_flush(info_w);
	}

	mb_fputwc('\n', ostream);
      }
    }
  else if (opt->mime_title) {
    char *mbline;
    size_t eol;

    while (*(mbline = getmbs_unlimited(&eol)))
      if (ostream)
	fputs(mb_mem2b64(mbline, &eol, opt->mime_title, &opt->osetup, ""), ostream);
  }
  else if (ostream) {
    mb_wchar_t wc;
    void *ws_beg, *ws_end;

    do {
      ws_beg = ws_end = linebuf;
      wc = mb_cs_detect_encode(info_r,
			       MB_ENCODE_TO_WS | MB_ENCODE_SKIP_INVALID | MB_ENCODE_SKIP_SHORT,
			       &ws_end, linebuf + BUFSIZ);

      if (converterv[0])
	mb_apply_convv(ws_beg, ws_end, converterv, info_w);

      mb_decode(ws_beg, ws_end, info_w);
    } while (wc != mb_notchar_eof);
  }
  else {
    mb_wchar_t wc;
    void *ws_end;

    do {
      ws_end = linebuf;
      wc = mb_cs_detect_encode(info_r,
			       MB_ENCODE_TO_WS | MB_ENCODE_SKIP_INVALID | MB_ENCODE_SKIP_SHORT,
			       &ws_end, linebuf + BUFSIZ);
    } while (wc != mb_notchar_eof);
  }

  if (opt->which) {
    const char *cs;

    cs = (info_r->ces && info_r->ces->namev && info_r->ces->namev[0]) ? info_r->ces->namev[0] : "UNKNOWN";

    if (real_argc > 1)
      fprintf(stderr, "%s: %s\n", fn ? fn : "-", cs);
    else
      fprintf(stderr, "%s\n", cs);
  }

  if (notstdin) {
    mb_fclose(istream);
    istream = stdin;
  }
}

#define INDENT "    "

static struct opt_tab_st {
  char *opt;
  size_t opt_len;
  int argc;
  void (*func)(struct opt_st *, char *, int, char **);
  char *argname, *usage;
} opt_tab[] = {
  {MB_KEY_DEF("-?"), 0, opt_help,
   NULL, "display this message and exit.\n"},
  {MB_KEY_DEF("-a"), 1, opt_appendtofile,
   "<file>", "output is appended to <file>.\n"},
  {MB_KEY_DEF("-c"), 1, opt_convertto,
   "<converters>",
   "specifies character encoding conversion.\n"
   "<converters> must be comma separated list of\n"
   INDENT "ascii\n"
   INDENT INDENT "domestic ASCII converted to US-ASCII,\n"
   INDENT "ces\n"
   INDENT INDENT "converted appropriately according to\n"
   INDENT INDENT "the CES bound to input/output stream,\n"
   INDENT "to-ucs\n"
   INDENT INDENT "converted to Unicode,\n"
   INDENT "f2h, full-to-half\n"
   INDENT INDENT "Fullwidth compatibility characters are converted to\n"
   INDENT INDENT "corresponding halfwidth ones,\n"
   INDENT "h2f, half-to-full\n"
   INDENT INDENT "Halfwidth compatibility characters are converted to\n"
   INDENT INDENT "corresponding fullwidth ones,\n"
   INDENT "jisx0213\n"
   INDENT INDENT "Codepoints in JIS C 6226 or in JIS X 0208\n"
   INDENT INDENT "which are bound to no character are converted\n"
   INDENT INDENT "into JIS X 0213 plane 1,\n"
   INDENT "jisx0213-aggressive\n"
   INDENT INDENT "All codepoints in JIS C 6226 or in JIS X 0208\n"
   INDENT INDENT "are converted into JIS X 0213 plane 1,\n"
   INDENT "ms-latin1\n"
   INDENT INDENT "Unicode characters of code point between 0x80 and\n"
   INDENT INDENT "0x9F (both inclusive) are converted to other\n"
   INDENT INDENT "Unicode characters as if they are characters of those\n"
   INDENT INDENT "code point in Microsoft Windows Codepage 1252,\n"
   INDENT "ucs-to-jis0208-extra, jis0208-to-ucs-extra\n"
   INDENT INDENT "Converters between some JIS X 0208 and Unicode characters\n"
   INDENT INDENT "having similar glyphs (by the courtesy of\n"
   INDENT INDENT "Ambrose Li E<lt>acli@ada.dhs.orgE<gt>).\n"},
  {MB_KEY_DEF("-f"), 1, opt_flag,
   "<flags>",
   "specifies flags to change behavior of conversion.\n"
   "<flags> must be comma separated list of\n"
   INDENT "28, use-0x28-for-94x94inG0\n"
   INDENT INDENT "use \"1/11 2/4 2/8 F\"\n"
   INDENT INDENT "instead of \"1/11 2/4 F\"\n"
   INDENT INDENT "to designate charsets with final octet\n"
   INDENT INDENT "4/0, 4/1, or 4/2 to G0,\n"
   INDENT "ac, ascii-at-control\n"
   INDENT INDENT "escape sequence \"1/11 2/8 4/2\" is\n"
   INDENT INDENT "output before every control character,\n"
   INDENT "nossl, ignore-7bit-single-shift\n"
   INDENT INDENT "escape sequence for 7 bit single shift\n"
   INDENT INDENT "is ignored,\n"
   INDENT "dnc, discard-notprefered-char\n"
   INDENT INDENT "discard characters which CES bound\n"
   INDENT INDENT "to output stream can not decode.\n"
  },
  {MB_KEY_DEF("-h"), 0, opt_help,
   "\n-?", NULL},
  {MB_KEY_DEF("-i"), 0, opt_input,
   NULL, "succeeding options apply to input stream.\n"},
  {MB_KEY_DEF("-m"), 1, opt_mimetitle,
   "<string>",
   "mime encoding conforming to RFC2047 is performed.\n"
   "<string> is used as charset name.\n"},
  {MB_KEY_DEF("-n"), 0, opt_lineno,
   NULL, "line number (>= 1) is inserted to beginning of each line.\n"},
  {MB_KEY_DEF("-o"), 0, opt_output,
   NULL, "succeeding options apply to output stream.\n"},
  {MB_KEY_DEF("-p"), 0, opt_cprop,
   NULL, "output properties of characters.\n"},
  {MB_KEY_DEF("-t"), 1, opt_tofile,
   "<file>", "output to file (truncated).\n"},
  {MB_KEY_DEF("-w"), 0, opt_width,
   NULL, "output width of each line.\n"},
  {MB_KEY_DEF("-cs"), 1, opt_charset,
   "<string>", "specifies charset name.\n"},
  {MB_KEY_DEF("--to"), 1, opt_tofile,
   "\n-t", NULL},
  {MB_KEY_DEF("--flag"), 1, opt_flag,
   "\n-f", NULL},
  {MB_KEY_DEF("--help"), 0, opt_help,
   "\n-?", NULL},
  {MB_KEY_DEF("--cname"), 1, opt_cname,
   "<canonical names>=<charset name>[,<charset name>,...]",
   "specifies canonical name of\n"
   "non-standard charset name.\n"},
  {MB_KEY_DEF("--input"), 0, opt_input,
   "\n-i", NULL},
  {MB_KEY_DEF("--regex"), 1, opt_regex,
   "[*<charset name>*]<regex>",
   "specifies regular expression to filter output.\n"
   "character encoding of expression is converted\n"
   "to that of output stream."},
  {MB_KEY_DEF("--which"), 0, opt_which,
   NULL, "output charset of input stream to stderr"},
  {MB_KEY_DEF("--width"), 0, opt_width,
   "\n-w", NULL},
  {MB_KEY_DEF("--format"), 1, opt_format,
   "<string>", "specifies output format\n"},
  {MB_KEY_DEF("--output"), 0, opt_output,
   "\n-o", NULL},
  {MB_KEY_DEF("--charset"), 1, opt_charset,
   "\n-cs", NULL},
  {MB_KEY_DEF("--append-to"), 1, opt_appendtofile,
   "\n-a", NULL},
  {MB_KEY_DEF("--convert-to"), 1, opt_convertto,
   "\n-c", NULL},
  {MB_KEY_DEF("--line-number"), 0, opt_lineno,
   "\n-n", NULL},
  {MB_KEY_DEF("--mime-charset"), 1, opt_mimetitle,
   "\n-m", NULL},
  {MB_KEY_DEF("--char-property"), 0, opt_cprop,
   "\n-p", NULL},
};

int
main(int argc, char *argv[])
{
  struct opt_st opt = {};
  struct opt_tab_st *p;
  char *eqsign;
  int i;

  opt.setup = &opt.isetup;
  opt.mime_title = opt.format = NULL;
  opt.iconv = opt.oconv = opt.lineno = 0;
  me = argv[0];
  ostream = stdout;

  for (i = 1 ; i < argc ;)
    if ((p = mb_nc_bsearch(argv[i], strlen(argv[i]), opt_tab,
			   offsetof(struct opt_tab_st, opt), offsetof(struct opt_tab_st, opt_len),
			   sizeof(opt_tab[0]), sizeof(opt_tab)))) {
      ++i;

      if (p->argc > 0) {
	if (p->argc <= argc - i)
	  p->func(&opt, argv[i], argc - i - 1, &argv[i + 1]);
	else
	  eexit(1, "%s: %s requires %d argument(s)\n", me, argv[i - 1], p->argc);
      }
      else
	p->func(&opt, NULL, 0, NULL);

      i += p->argc;
    }
    else if ((eqsign = strchr(argv[i], '=')) &&
	     (p = mb_nc_bsearch(argv[i], eqsign - argv[i], opt_tab,
				offsetof(struct opt_tab_st, opt), offsetof(struct opt_tab_st, opt_len),
				sizeof(opt_tab[0]), sizeof(opt_tab)))) {
      if (p->argc > 0) {
	if (p->argc <= argc - i)
	  p->func(&opt, &eqsign[1], argc - i - 1, &argv[i + 1]);
	else
	  eexit(1, "%s: %s requires more %d argument(s)\n", me, argv[i], p->argc - 1);
      }
      else {
	p->func(&opt, NULL, 0, NULL);
	++i;
      }

      i += p->argc;
    }
    else
      break;

  if (ostream) {
    if (!opt.oconv)
      mb_setsetup(&opt.osetup, "@", "x-moe-internal");

    mb_fbind(ostream, "a!", &opt.osetup);

    if (!mb_finfo(ostream, NULL, &info_w))
      eexit(1, "%s: mb_fbind(ostream, ...): %s\n", me, strerror(errno));
  }

  if (opt.regex) {
    mb_setsetup(&regex_mb_setup, "@", opt.regex_cs);
    mb_mem2mb_setup(&regex_mb_info, opt.regex, strlen(opt.regex), &regex_mb_setup, "");
    regex_nfa = wcrx_compile(opt.regex_ci ? &regex_ci_desc : &regex_desc);
  }

  if (i < argc) {
    int real_argc = argc - i;

    do {
      doit(&opt, argv[i++], real_argc);
    } while (i < argc);
  }
  else
    doit(&opt, NULL, 0);

  if (ostream)
    mb_fclose(ostream);

  return 0;
}

static void
show_usage(char *usage)
{
  int j, k;

  for (j = 0 ;; j += k + 1) {
    k = strcspn(&usage[j], "\n");

    if (k || usage[j + k])
      fprintf(stderr, INDENT "%.*s\n", k, &usage[j]);

    if (!usage[j + k])
      break;
  }
}

static void
opt_help(struct opt_st *p_opt, char *s, int argrestc, char **argrestv)
{
  size_t i;

  fprintf(stderr, "%s (supported by libmoe-%s) [options] file ...\nsummary of options:\n",
	  me, mb_version_string);

  for (i = 0 ; i < sizeof(opt_tab) / sizeof(opt_tab[0]) ; ++i)
    if (opt_tab[i].argname) {
      if (opt_tab[i].argname[0] == '\n') {
	if (opt_tab[i].argc) {
	  if (!strncmp(opt_tab[i].opt, "--", 2))
	    fprintf(stderr, "\n%s=<arg>: same as \"%s <arg>\"\n", opt_tab[i].opt, &opt_tab[i].argname[1]);
	  else
	    fprintf(stderr, "\n%s <arg>: same as \"%s <arg>\"\n", opt_tab[i].opt, &opt_tab[i].argname[1]);
	}
	else
	  fprintf(stderr, "\n%s: same as \"%s\"\n", opt_tab[i].opt, &opt_tab[i].argname[1]);
      }
      else {
	if (!strncmp(opt_tab[i].opt, "--", 2))
	  fprintf(stderr, "\n%s=%s\n", opt_tab[i].opt, opt_tab[i].argname);
	else
	  fprintf(stderr, "\n%s %s\n", opt_tab[i].opt, opt_tab[i].argname);

	show_usage(opt_tab[i].usage);
      }
    }
    else {
      fprintf(stderr, "\n%s\n", opt_tab[i].opt);
      show_usage(opt_tab[i].usage);
    }

  exit(0);
}
