/******************************************************************************\
* Copyright (c) 2019, Robert van Engelen, Genivia Inc. All rights reserved.    *
*                                                                              *
* Redistribution and use in source and binary forms, with or without           *
* modification, are permitted provided that the following conditions are met:  *
*                                                                              *
*   (1) Redistributions of source code must retain the above copyright notice, *
*       this list of conditions and the following disclaimer.                  *
*                                                                              *
*   (2) Redistributions in binary form must reproduce the above copyright      *
*       notice, this list of conditions and the following disclaimer in the    *
*       documentation and/or other materials provided with the distribution.   *
*                                                                              *
*   (3) The name of the author may not be used to endorse or promote products  *
*       derived from this software without specific prior written permission.  *
*                                                                              *
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED *
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF         *
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO   *
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,       *
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, *
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;  *
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,     *
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR      *
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF       *
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                                   *
\******************************************************************************/

/**
@file      output.cpp
@brief     Output management
@author    Robert van Engelen - engelen@genivia.com
@copyright (c) 2019-2025, Robert van Engelen, Genivia Inc. All rights reserved.
@copyright (c) BSD-3 License - see LICENSE.txt
*/

#include "output.hpp"

// dump matching data in hex
void Output::Dump::hex(short mode, size_t byte_offset, const char *data, size_t size)
{
  offset = byte_offset;
  while (size > 0)
  {
    bytes[offset++ % flag_hex_columns] = (mode << 8) | *reinterpret_cast<const unsigned char*>(data++);
    if (offset % flag_hex_columns == 0)
      line();
    --size;
  }
}

// dump one line of hex
void Output::Dump::line()
{
  if (flag_hex_star)
  {
    // if the previous hex line was the same as this hex line, output a * (but only once)
    size_t i;

    for (i = 0; i < flag_hex_columns; ++i)
      if (prevb[i] < 0 || bytes[i] != prevb[i])
        break;

    if (i >= flag_hex_columns)
    {
      if (!pstar)
      {
        out.str(color_se);
        out.chr('*');
        out.nl();

        pstar = true;
      }

      for (i = 0; i < MAX_HEX_COLUMNS; ++i)
        bytes[i] = -1;

      return;
    }
  }

  out.str(color_bn);
  out.hex((offset - 1) - (offset - 1) % flag_hex_columns, 8);
  out.str(color_off);
  out.chr(' ');

  short last_hex_color = HEX_MAX;

  for (size_t i = 0; i < flag_hex_columns; ++i)
  {
    if (bytes[i] < 0)
    {
      if (last_hex_color != -1)
      {
        last_hex_color = -1;
        out.str(color_off);
        out.str(color_cx);
      }
      if (flag_hex_hbr || (i == 0 && flag_hex_cbr))
        out.chr(' ');
      out.str("--", 2);
      if (flag_hex_cbr && (i & 7) == 7)
        out.chr(' ');
    }
    else
    {
      short byte = bytes[i];
      if ((byte >> 8) != last_hex_color)
      {
        out.str(last_hex_color == HEX_MATCH || last_hex_color == HEX_CONTEXT_MATCH ? match_off : color_off);
        last_hex_color = byte >> 8;
        out.str(color_hex[last_hex_color]);
      }
      if (flag_hex_hbr || (i == 0 && flag_hex_cbr))
        out.chr(' ');
      out.hex(byte & 0xff, 2);
      if (flag_hex_cbr && (i & 7) == 7)
        out.chr(' ');
    }
  }

  out.str(color_off);

  if (flag_hex_chr)
  {
    out.str(color_se);
    if (flag_hex_hbr)
      out.chr(' ');
    out.chr('|');

    last_hex_color = HEX_MAX;
    bool inverted = false;

    for (size_t i = 0; i < flag_hex_columns; ++i)
    {
      if (bytes[i] < 0)
      {
        if (last_hex_color != -1)
        {
          last_hex_color = -1;
          out.str(color_off);
          out.str(color_cx);
        }
        out.chr('-');
      }
      else
      {
        short byte = bytes[i];
        if ((byte >> 8) != last_hex_color)
        {
          out.str(last_hex_color == HEX_MATCH || last_hex_color == HEX_CONTEXT_MATCH ? match_off : color_off);
          last_hex_color = byte >> 8;
          out.str(color_hex[last_hex_color]);
        }
        byte &= 0xff;
        if (flag_color != NULL)
        {
          if (byte < 0x20)
          {
            out.str("\033[7m", 4);
            out.chr('@' + byte);
            inverted = true;
          }
          else if (byte == 0x7f)
          {
            out.str("\033[7m~", 5);
            inverted = true;
          }
          else if (byte > 0x7f)
          {
            out.str("\033[7m.", 5);
            inverted = true;
          }
          else if (inverted)
          {
            out.str(color_off);
            out.str(color_hex[last_hex_color]);
            out.chr(byte);
            inverted = false;
          }
          else
          {
            out.chr(byte);
          }
        }
        else if (byte < 0x20 || byte >= 0x7f)
        {
          out.chr('.');
        }
        else
        {
          out.chr(byte);
        }
      }
    }

    out.str(color_off);
    out.str(color_se);
    out.chr('|');
    out.str(color_off);
  }

  out.nl();

  for (int i = 0; i < MAX_HEX_COLUMNS; ++i)
  {
    prevb[i] = bytes[i];
    bytes[i] = -1;
  }

  pstar = false;
}

// output color when set
void Output::color(const char *arg)
{
  if (arg != NULL)
  {
    static const char *colors[10] = {
      color_sl,
      color_cx,
      color_mt,
      color_ms,
      color_mc,
      color_fn,
      color_ln,
      color_cn,
      color_bn,
      color_se,
    };
    static const char *codes = "sl cx mt ms mc fn ln cn bn se ";
    char code[4] = { arg[0], arg[1], ' ', 0 };
    const char *s = strstr(codes, code);
    if (s != NULL)
      str(colors[(s - codes) / 3]);
  }
  else
  {
    str(color_off);
  }
}

// output the header part of the match, preceding the matched line
void Output::header(const char *pathname, const std::string& partname, bool& heading, size_t lineno, reflex::AbstractMatcher *matcher, size_t byte_offset, const char *separator, bool newline)
{
  // if hex dump line is incomplete and a header is output, then complete the hex dump first
  if (dump.incomplete() &&
      (heading ||
       (!flag_no_filename && !partname.empty()) ||
       flag_line_number ||
       flag_column_number ||
       flag_byte_offset))
    dump.done();

  // get column number when we need it
  size_t columno = flag_column_number && matcher != NULL ? matcher->columno() + 1 : 1;

  // -Q: mark pathname with three \0 markers in headings, unless -a
  bool nul = flag_query && heading && !flag_text;

  if (nul)
    chr('\0');

  // --hyperlink: open link, unless standard input
  bool hyp = pathname != Static::LABEL_STANDARD_INPUT && color_hl != NULL; // include hyperlinks

  if (hyp)
    open_hyperlink(pathname, !(heading && flag_heading) && flag_hyperlink_line, lineno, columno);

  // when a separator is needed
  bool sep = false;

  // header should include the pathname
  if (heading)
  {
    str(color_fn);

    if (nul)
      chr('\0');

    str(pathname);

    if (nul)
      chr('\0');

    str(color_off);

    if (flag_null)
      chr('\0');

    if (flag_heading)
    {
      // --hyperlink: close link
      if (hyp)
        close_hyperlink();

      str(color_fn);
      str(color_del);
      str(color_off);
      nl();

      // --hyperlink: open link
      if (hyp)
        open_hyperlink(pathname, flag_hyperlink_line, lineno, columno);

      // the next headers should not include this pathname
      heading = false;
    }
    else
    {
      sep = !flag_null;
    }
  }

  if (!flag_no_filename && !partname.empty())
  {
    nul = flag_query && !flag_text && (flag_heading || !nul);

    if (nul)
      chr('\0');

    str(color_fn);

    if (nul)
      chr('\0');

    chr('{');
    str(partname);
    chr('}');

    if (nul)
      chr('\0');

    str(color_off);

    sep = true;
  }

  if (flag_line_number)
  {
    if (sep)
    {
      str(color_se);
      str(separator);
      str(color_off);
    }

    str(color_ln);
    num(lineno, (flag_initial_tab ? 6 : 1));
    str(color_off);

    sep = true;
  }

  if (flag_column_number)
  {
    if (sep)
    {
      str(color_se);
      str(separator);
      str(color_off);
    }

    str(color_cn);
    num(columno, (flag_initial_tab ? 3 : 1));
    str(color_off);

    sep = true;
  }

  // --hyperlink: close link
  if (hyp)
    close_hyperlink();

  if (flag_byte_offset)
  {
    if (sep)
    {
      str(color_se);
      str(separator);
      str(color_off);
    }

    str(color_bn);
    num(byte_offset, (flag_initial_tab ? 7 : 1));
    str(color_off);

    sep = true;
  }

  if (sep)
  {
    str(color_se);
    str(separator);
    str(color_off);

    if (flag_initial_tab)
      chr('\t');

    if (newline)
      nl();
  }
}

// output the short pathname header for --files_with_matches and --count
void Output::header(const char *pathname, const std::string& partname)
{
  bool hyp = pathname != Static::LABEL_STANDARD_INPUT && color_hl != NULL; // include hyperlinks
  bool nul = flag_query; // -Q: mark pathname with three \0 markers for quick navigation

  if (flag_tree)
  {
    // acquire lock on output and to access global Tree::path and Tree::depth
    acquire();

    int up = 0;

    while (!Tree::path.empty() && Tree::path.compare(0, Tree::path.size(), pathname, Tree::path.size()) != 0)
    {
      Tree::path.pop_back();

      size_t len = Tree::path.rfind(PATHSEPCHR);

      if (len == std::string::npos)
        Tree::path.clear();
      else
        Tree::path.resize(len + 1);

      ++up;
      --Tree::depth;
    }

    if (up > 0)
    {
      for (int i = 0; i < Tree::depth; ++i)
        str(Tree::bar);
      for (int i = 1; i < up; ++i)
        str(Tree::end);
      nl();

      // make sure to add a break between trees with terminated leafs
      if (up > 1 && *Tree::end != '\0' && Tree::depth == 0)
        nl();
    }
    else if (Tree::path.empty() && strchr(pathname, PATHSEPCHR) != NULL)
    {
      // add a break between the list of filenames without path and filenames with paths
      nl();
    }

    const char *sep;

    while ((sep = strchr(pathname + Tree::path.size(), PATHSEPCHR)) != NULL)
    {
      if (nul)
        chr('\0');

      for (int i = 1; i < Tree::depth; ++i)
        str(Tree::bar);

      if (Tree::depth > 0)
        str(Tree::ptr);

      if (nul)
        chr('\0');

      str(pathname + Tree::path.size(), sep - (pathname + Tree::path.size()) + 1);

      if (nul)
        chr('\0');

      nl();

      Tree::path.assign(pathname, sep - pathname + 1);
      ++Tree::depth;
    }

    if (nul)
      chr('\0');

    for (int i = 1; i < Tree::depth; ++i)
      str(Tree::bar);

    if (Tree::depth > 0)
      str(Tree::ptr);

    str(color_fn);

    if (hyp)
      open_hyperlink(pathname);

    if (nul)
      chr('\0');

    str(pathname + Tree::path.size());

    if (nul)
      chr('\0');

  }
  else
  {
    if (nul)
      chr('\0');

    str(color_fn);

    if (hyp)
      open_hyperlink(pathname);

    if (nul)
      chr('\0');

    str(pathname);

    if (nul)
      chr('\0');

    if (hyp)
      close_hyperlink();
  }

  if (!partname.empty())
  {
    chr('{');
    str(partname);
    chr('}');
  }

  str(color_off);
}

// output "Binary file ... matches"
void Output::binary_file_matches(const char *pathname, const std::string& partname)
{
  if ((mode_ & BINARY) != 0)
    return;

  str(color_off);
  str("Binary file ");
  str(color_fn);

  if (pathname != Static::LABEL_STANDARD_INPUT && color_hl != NULL)
    open_hyperlink(pathname);

  str(pathname);

  if (pathname != Static::LABEL_STANDARD_INPUT && color_hl != NULL)
    close_hyperlink();

  if (!partname.empty())
  {
    chr('{');
    str(partname);
    chr('}');
  }

  str(color_off);
  str(" matches");
  nl();

  mode_ |= BINARY;
}

// get a group capture's string pointer and size specified by %[ARG] as arg, if any
std::pair<const char*,size_t> Output::capture(reflex::AbstractMatcher *matcher, const char *arg)
{
  if (arg != NULL)
  {
    while (true)
    {
      const char *bar = strchr(arg, '|');
      const char *end = strchr(arg, ']');

      if (end == NULL)
        break;

      if (bar == NULL || bar > end)
        bar = end;

      if (isdigit(static_cast<unsigned char>(*arg)))
      {
        size_t index = strtoul(arg, NULL, 10);

        if (index == 0 || *bar == ']')
          return (*matcher)[index];

        std::pair<size_t,const char*> id = matcher->group_id();

        while (id.first != 0 && id.first != index)
          id = matcher->group_next_id();

        if (id.first == index)
          return (*matcher)[index];
      }
      else
      {
        std::pair<size_t,const char*> id = matcher->group_id();

        while (id.first != 0
            && (id.second == NULL || strncmp(id.second, arg, bar - arg) != 0 || id.second[bar - arg] != '\0'))
          id = matcher->group_next_id();

        if (id.first != 0)
          return (*matcher)[id.first];
      }

      if (*bar == ']')
        break;

      arg = bar + 1;
    }
  }

  return std::pair<const char*,size_t>(NULL, 0);
}

// output format with option --format-begin and --format-end
void Output::format(const char *format, size_t matches)
{
  const char *sep = NULL;
  size_t len = 0;
  const char *s = format;
  while (*s != '\0')
  {
    int width = 0;
    const char *arg = NULL;
    const char *t = s;

    while (*s != '\0' && *s != '%')
      ++s;
    str(t, s - t);
    if (*s == '\0' || *(s + 1) == '\0')
      break;
    ++s;
    if (*s == '{')
    {
      char *r = NULL;
      width = strtol(s + 1, &r, 10);
      if (r != NULL && *r == '}')
        s = r + 1;
    }
    if (*s == '[')
    {
      arg = ++s;
      while (*s != '\0' && *s != ']')
        ++s;
      if (*s == '\0' || *(s + 1) == '\0')
        break;
      ++s;
    }

    int c = *s;

    switch (c)
    {
      case 'T':
        if (flag_initial_tab)
        {
          if (arg)
            str(arg, s - arg - 1);
          chr('\t');
        }
        break;

      case 'S':
        if (matches > 1)
        {
          if (arg)
            str(arg, s - arg - 1);
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case '$':
        sep = arg;
        if (arg != NULL)
          len = s - arg - 1;
        break;

      case 't':
        chr('\t');
        break;

      case 's':
        if (sep != NULL)
          str(sep, len);
        else
          str(flag_separator);
        break;

      case 'R':
        if (!flag_break)
          break;
        // FALLTHROUGH

      case '~':
#ifdef OS_WIN
        chr('\r');
#endif
        chr('\n');
        break;

      case 'm':
        num(matches, width);
        break;

      case 'U':
        if (arg != NULL)
          wchr(strtol(arg, NULL, 16));
        break;

      case '=':
        color(arg);
        break;

      case '<':
        if (matches <= 1 && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case '>':
        if (matches > 1 && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case ',':
      case ':':
      case ';':
      case '|':
        if (matches > 1)
          chr(c);
        break;

      default:
        chr(c);
    }
    ++s;
  }
}

// output formatted match with options --format, --format-open, --format-close
bool Output::format(const char *format, const char *pathname, const std::string& partname, size_t matches, size_t *matching, reflex::AbstractMatcher *matcher, bool& heading, bool body, bool next)
{
  if (!body)
    lineno_ = 0;
  else if (lineno_ > 0 && lineno_ == matcher->lineno() && matcher->lines() == 1)
    return false;

  if (matching != NULL)
    ++*matching;

  size_t len = 0;
  const char *sep = NULL;
  const char *s = format;

  while (*s != '\0')
  {
    bool plus = false;
    int width = 0;
    const char *arg = NULL;
    const char *t = s;

    while (*s != '\0' && *s != '%')
      ++s;
    str(t, s - t);
    if (*s == '\0' || *(s + 1) == '\0')
      break;
    ++s;
    if (*s == '{')
    {
      char *r = NULL;
      plus = (s[1] == '+');
      width = strtol(s + 1, &r, 10);
      if (r != NULL && *r == '}')
        s = r + 1;
    }
    if (*s == '[')
    {
      arg = ++s;
      while (*s != '\0' && *s != ']')
        ++s;
      if (*s == '\0' || *(s + 1) == '\0')
        break;
      ++s;
    }

    int c = *s;

    switch (c)
    {
      case '+':
        if (flag_heading)
        {
          if (flag_with_filename)
          {
            if (heading)
            {
              if (arg != NULL)
                str(arg, s - arg - 1);
              str(pathname);
              if (flag_null)
                chr('\0');
              nl();
              heading = false;
            }
            else if (flag_break)
            {
              nl();
            }
          }
        }
        break;

      case 'F':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (heading)
            str(pathname);
          if (!partname.empty())
          {
            chr('{');
            str(partname);
            chr('}');
          }
          if (flag_null)
            chr('\0');
          else if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'f':
        str(pathname);
        if (!partname.empty())
        {
          chr('{');
          str(partname);
          chr('}');
        }
        break;

      case 'a':
        {
          const char *basename = strrchr(pathname, PATHSEPCHR);
          if (basename == NULL)
            str(pathname);
          else
            str(basename + 1);
        }
        break;

      case 'p':
        {
          const char *basename = strrchr(pathname, PATHSEPCHR);
          if (basename != NULL)
            str(pathname, basename - pathname);
        }
        break;

      case 'z':
        str(partname);
        break;

      case 'H':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (!partname.empty())
          {
            std::string name;
            if (heading)
              name = pathname;
            name.push_back('{');
            name.append(partname);
            name.push_back('}');
            quote(name.c_str(), name.size());
          }
          else
          {
            quote(pathname, strlen(pathname));
          }
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'h':
        if (!partname.empty())
        {
          std::string name(pathname);
          name.push_back('{');
          name.append(partname);
          name.push_back('}');
          quote(name.c_str(), name.size());
        }
        else
        {
          quote(pathname, strlen(pathname));
        }
        break;

      case 'I':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (!partname.empty())
          {
            std::string name;
            if (heading)
              name = pathname;
            name.push_back('{');
            name.append(partname);
            name.push_back('}');
            xml(name.c_str(), name.size());
          }
          else
          {
            xml(pathname, strlen(pathname));
          }
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'i':
        if (!partname.empty())
        {
          std::string name(pathname);
          name.push_back('{');
          name.append(partname);
          name.push_back('}');
          xml(name.c_str(), name.size());
        }
        else
        {
          xml(pathname, strlen(pathname));
        }
        break;

      case 'N':
        if (flag_line_number)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          num(matcher->lineno(), (arg == NULL && flag_initial_tab ? 6 : width));
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'n':
        num(matcher->lineno(), width);
        break;

      case 'L':
        num(matcher->lines(), width);
        break;

      case 'l':
        num(matcher->lineno_end(), width);
        break;

      case 'K':
        if (flag_column_number)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          num(matcher->columno() + 1, (arg == NULL && flag_initial_tab ? 3 : width));
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'k':
        num(matcher->columno() + 1, width);
        break;

      case 'A':
        hex(matcher->first(), 8);
        chr('-');
        hex(matcher->last() - 1, 8);
        break;

      case 'B':
        if (flag_byte_offset)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          num(matcher->first(), width);
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'b':
        if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            num(cap.first - matcher->text() + matcher->first(), width);
        }
        else
        {
          num(matcher->first(), width);
        }
        break;

      case 'T':
        if (flag_initial_tab)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          chr('\t');
        }
        break;

      case 't':
        chr('\t');
        break;

      case 'S':
        if (next)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 's':
        if (sep != NULL)
          str(sep, len);
        else
          str(flag_separator);
        break;

      case 'w':
        num(matcher->wsize(), width);
        break;

      case 'd':
        if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            num(cap.second, width);
        }
        else
        {
          num(matcher->size(), width);
        }
        break;

      case 'e':
        if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            num(cap.first + cap.second - matcher->text() + matcher->first(), width);
        }
        else
        {
          num(matcher->last(), width);
        }
        break;

      case 'G':
        {
          bool colon = false;
          std::pair<size_t,const char*> id = matcher->group_id();

          while (id.first != 0)
          {
            if (colon)
            {
              if (sep != NULL)
                str(sep, len);
              else
                str(flag_separator);
            }

            colon = true;

            if (arg != NULL)
            {
              size_t n = id.first;
              const char *bar;
              const char *end;

              while (true)
              {
                bar = strchr(arg, '|');
                end = strchr(arg, ']');

                --n;

                if (bar == NULL || (end != NULL && bar > end))
                {
                  bar = end;
                  break;
                }

                if (n == 0)
                  break;

                arg = bar + 1;
              }

              if (n == 0 && bar != NULL)
                str(arg, bar - arg);
              else if (id.second != NULL)
                str(id.second);
              else
                num(id.first);
            }
            else
            {
              if (id.second != NULL)
                str(id.second);
              else
                num(id.first);
            }

            id = matcher->group_next_id();
          }
        }
        break;

      case 'g':
        if (arg != NULL)
        {
          std::pair<size_t,const char*> id = matcher->group_id();
          size_t n = id.first;

          if (n > 0)
          {
            const char *bar;
            const char *end;

            while (true)
            {
              bar = strchr(arg, '|');
              end = strchr(arg, ']');

              --n;

              if (bar == NULL || (end != NULL && bar > end))
              {
                bar = end;
                break;
              }

              if (n == 0)
                break;

              arg = bar + 1;
            }

            if (n == 0 && bar != NULL)
              str(arg, bar - arg);
            else if (id.second != NULL)
              str(id.second);
            else
              num(id.first);
          }
        }
        else
        {
          num(matcher->accept());
        }
        break;

      case 'M':
        num(matches, width);
        break;

      case 'm':
        num(matching == NULL ? matches : *matching, width);
        break;

      case 'O':
        mat(matcher, width);
        break;

      case 'o':
      case '#':
        if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
          {
            const char *p = cap.first;
            size_t n = cap.second;
            if (flag_hex || (flag_with_hex && !reflex::isutf8(p, p + n)))
            {
              if (width > 0 && static_cast<size_t>(width) < n)
                n = width;
              hex(p, n);
            }
            else
            {
              if (width > 0)
                utf8strn(p, n, width);
              else
                str(p, n);
            }
          }
        }
        else
        {
          const char *p = matcher->begin();
          size_t n = matcher->size();
          if (flag_hex || (flag_with_hex && !reflex::isutf8(p, p + n)))
          {
            if (width > 0 && static_cast<size_t>(width) < n)
              n = width;
            hex(p, n);
          }
          else
          {
            if (width != 0)
              p = match_context(matcher, plus, width, n);
            str(p, n);
          }
        }
        break;

      case 'Q':
        quote(matcher, width);
        break;

      case 'q':
        if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            quote(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          quote(context, size);
        }
        break;

      case 'C':
        if (flag_files_with_matches)
          str(flag_invert_match ? "\"false\"" : "\"true\"");
        else if (flag_count)
          chr('"'), num(matches), chr('"');
        else
          cpp(matcher, width);
        break;

      case 'c':
        if (flag_files_with_matches)
        {
          str(flag_invert_match ? "\"false\"" : "\"true\"");
        }
        else if (flag_count)
        {
          chr('"'), num(matches), chr('"');
        }
        else if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            cpp(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          cpp(context, size);
        }
        break;

      case 'V':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          csv(matcher, width);
        break;

      case 'v':
        if (flag_files_with_matches)
        {
          str(flag_invert_match ? "false" : "true");
        }
        else if (flag_count)
        {
          num(matches);
        }
        else if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            csv(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          csv(context, size);
        }
        break;

      case 'J':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          json(matcher, width);
        break;

      case 'j':
        if (flag_files_with_matches)
        {
          str(flag_invert_match ? "false" : "true");
        }
        else if (flag_count)
        {
          num(matches);
        }
        else if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            json(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          json(context, size);
        }
        break;

      case 'X':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          xml(matcher, width);
        break;

      case 'x':
        if (flag_files_with_matches)
        {
          str(flag_invert_match ? "false" : "true");
        }
        else if (flag_count)
        {
          num(matches);
        }
        else if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            xml(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          xml(context, size);
        }
        break;

      case 'Y':
        if (flag_files_with_matches)
          hex(flag_invert_match ? 0 : 1);
        else if (flag_count)
          hex(matches);
        else
          hex(matcher, width);
        break;

      case 'y':
        if (flag_files_with_matches)
        {
          str(flag_invert_match ? "false" : "true");
        }
        else if (flag_count)
        {
          num(matches);
        }
        else if (arg != NULL)
        {
          std::pair<const char*,size_t> cap = capture(matcher, arg);
          if (cap.first != NULL)
            hex(cap.first, width > 0 ? utf8cut(cap.first, cap.second, width) : cap.second);
        }
        else
        {
          size_t size;
          const char *context = match_context(matcher, plus, width, size);
          hex(context, size);
        }
        break;

      case 'Z':
        if (flag_fuzzy > 0)
        {
          if (!flag_match)
          {
            // -Z: we used the fuzzy matcher to search, so a dynamic cast is fine
            reflex::FuzzyMatcher *fuzzy_matcher = dynamic_cast<reflex::FuzzyMatcher*>(matcher);
            if (!flag_files_with_matches && !flag_count)
              num(fuzzy_matcher->edits(), width);
            else
              num(fuzzy_matcher->distance() & 0xff, width);
          }
          else
          {
            // --match all
            chr('0');
          }
        }
        break;

      case 'u':
        if (!flag_ungroup)
          lineno_ = matcher->lineno();
        break;

      case '$':
        sep = arg;
        if (arg != NULL)
          len = s - arg - 1;
        break;

      case 'R':
        if (!flag_break)
          break;
        // FALLTHROUGH

      case '~':
#ifdef OS_WIN
        chr('\r');
#endif
        chr('\n');
        break;

      case 'U':
        if (arg != NULL)
          wchr(strtol(arg, NULL, 16));
        break;

      case '=':
        color(arg);
        break;

      case '<':
        if (!next && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case '>':
        if (next && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case ',':
      case ':':
      case ';':
      case '|':
        if (next)
          chr(c);
        break;

      default:
        chr(c);
        break;

      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
        std::pair<const char*,size_t> capture = (*matcher)[c - '0'];
        str(capture.first, capture.second);
        break;
    }
    ++s;
  }

  return true;
}

// output formatted match with options -v --format
void Output::format_invert(const char *format, const char *pathname, const std::string& partname, size_t matches, size_t lineno, size_t offset, const char *ptr, size_t size, bool& heading, bool next)
{
  size_t len = 0;
  const char *sep = NULL;
  const char *s = format;

  while (*s != '\0')
  {
    int width = 0;
    const char *arg = NULL;
    const char *t = s;

    while (*s != '\0' && *s != '%')
      ++s;
    str(t, s - t);
    if (*s == '\0' || *(s + 1) == '\0')
      break;
    ++s;
    if (*s == '{')
    {
      char *r = NULL;
      width = strtol(s + 1, &r, 10);
      if (r != NULL && *r == '}')
        s = r + 1;
    }
    if (*s == '[')
    {
      arg = ++s;
      while (*s != '\0' && *s != ']')
        ++s;
      if (*s == '\0' || *(s + 1) == '\0')
        break;
      ++s;
    }

    int c = *s;

    switch (c)
    {
      case '+':
        if (flag_heading)
        {
          if (flag_with_filename)
          {
            if (heading)
            {
              if (arg != NULL)
                str(arg, s - arg - 1);
              str(pathname);
              if (flag_null)
                chr('\0');
              nl();
              heading = false;
            }
            else if (flag_break)
            {
              nl();
            }
          }
        }
        break;

      case 'F':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (heading)
            str(pathname);
          if (!partname.empty())
          {
            chr('{');
            str(partname);
            chr('}');
          }
          if (flag_null)
            chr('\0');
          else if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'f':
        str(pathname);
        if (!partname.empty())
        {
          chr('{');
          str(partname);
          chr('}');
        }
        break;

      case 'a':
        {
          const char *basename = strrchr(pathname, PATHSEPCHR);
          if (basename == NULL)
            str(pathname);
          else
            str(basename + 1);
        }
        break;

      case 'p':
        {
          const char *basename = strrchr(pathname, PATHSEPCHR);
          if (basename != NULL)
            str(pathname, basename - pathname);
        }
        break;

      case 'z':
        str(partname);
        break;

      case 'H':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (!partname.empty())
          {
            std::string name;
            if (heading)
              name = pathname;
            name.push_back('{');
            name.append(partname);
            name.push_back('}');
            quote(name.c_str(), name.size());
          }
          else
          {
            quote(pathname, strlen(pathname));
          }
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'h':
        if (!partname.empty())
        {
          std::string name(pathname);
          name.push_back('{');
          name.append(partname);
          name.push_back('}');
          quote(name.c_str(), name.size());
        }
        else
        {
          quote(pathname, strlen(pathname));
        }
        break;

      case 'I':
        if (flag_with_filename && (heading || !partname.empty()))
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (!partname.empty())
          {
            std::string name;
            if (heading)
              name = pathname;
            name.push_back('{');
            name.append(partname);
            name.push_back('}');
            xml(name.c_str(), name.size());
          }
          else
          {
            xml(pathname, strlen(pathname));
          }
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'i':
        if (!partname.empty())
        {
          std::string name(pathname);
          name.push_back('{');
          name.append(partname);
          name.push_back('}');
          xml(name.c_str(), name.size());
        }
        else
        {
          xml(pathname, strlen(pathname));
        }
        break;

      case 'N':
        if (flag_line_number)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          num(lineno, (arg == NULL && flag_initial_tab ? 6 : width));
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'n':
        num(lineno, width);
        break;

      case 'K':
        if (flag_column_number)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          chr('1');
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'k':
        chr('1');
        break;

      case 'A':
        hex(offset, 8);
        chr('-');
        hex(offset + size - 1, 8);
        break;

      case 'B':
        if (flag_byte_offset)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          num(offset, width);
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 'b':
        num(offset, width);
        break;

      case 'T':
        if (flag_initial_tab)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          chr('\t');
        }
        break;

      case 't':
        chr('\t');
        break;

      case 'S':
        if (next)
        {
          if (arg != NULL)
            str(arg, s - arg - 1);
          if (sep != NULL)
            str(sep, len);
          else
            str(flag_separator);
        }
        break;

      case 's':
        if (sep != NULL)
          str(sep, len);
        else
          str(flag_separator);
        break;

      case 'w':
      {
        size_t n = 0;
        for (const char *p = ptr; p < ptr + size; ++p)
          n += (*p & 0xC0) != 0x80;
        num(n, width);
        break;
      }

      case 'd':
        num(size, width);
        break;

      case 'e':
        num(offset + size, width);
        break;

      case 'G':
      case 'g':
        break;

      case 'm':
        num(matches, width);
        break;

      case 'O':
      case 'o':
        str(ptr, size);
        break;

      case 'Q':
      case 'q':
        quote(ptr, size);
        break;

      case 'C':
      case 'c':
        if (flag_files_with_matches)
          str(flag_invert_match ? "\"false\"" : "\"true\"");
        else if (flag_count)
          chr('"'), num(matches), chr('"');
        else
          cpp(ptr, size);
        break;

      case 'V':
      case 'v':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          csv(ptr, size);
        break;

      case 'J':
      case 'j':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          json(ptr, size);
        break;

      case 'X':
      case 'x':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          xml(ptr, size);
        break;

      case 'Y':
      case 'y':
        if (flag_files_with_matches)
          str(flag_invert_match ? "false" : "true");
        else if (flag_count)
          num(matches);
        else
          hex(ptr, size);
        break;

      case 'Z':
        break;

      case 'u':
        break;

      case '$':
        sep = arg;
        if (arg != NULL)
          len = s - arg - 1;
        break;

      case 'R':
        if (!flag_break)
          break;
        // FALLTHROUGH

      case '~':
#ifdef OS_WIN
        chr('\r');
#endif
        chr('\n');
        break;

      case 'U':
        if (arg != NULL)
          wchr(strtol(arg, NULL, 16));
        break;

      case '=':
        color(arg);
        break;

      case '<':
        if (!next && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case '>':
        if (next && arg != NULL)
          str(arg, s - arg - 1);
        break;

      case ',':
      case ':':
      case ';':
      case '|':
        if (next)
          chr(c);
        break;

      case '#':
      case '0':
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
        break;

      default:
        chr(c);
        break;
    }
    ++s;
  }
}

// output a quoted string with escapes for \ and "
void Output::quote(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;
  const char *t = s;

  chr('"');

  while (s < e)
  {
    if (*s == '\\' || *s == '"')
    {
      str(t, s - t);
      t = s;
      chr('\\');
    }
    ++s;
  }
  str(t, s - t);

  chr('"');
}

// output string in C/C++
void Output::cpp(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;
  const char *t = s;

  chr('"');

  while (s < e)
  {
    int c = *s;

    if ((c & 0x80) == 0)
    {
      if (c < 0x20 || c == '"' || c == '\\')
      {
        str(t, s - t);
        t = s + 1;

        switch (c)
        {
          case '\b':
            c = 'b';
            break;

          case '\f':
            c = 'f';
            break;

          case '\n':
            c = 'n';
            break;

          case '\r':
            c = 'r';
            break;

          case '\t':
            c = 't';
            break;
        }

        chr('\\');

        if (c > 0x20)
          chr(c);
        else
          oct(c);
      }
    }
    ++s;
  }
  str(t, s - t);

  chr('"');
}

// output quoted string in CSV
void Output::csv(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;
  const char *t = s;

  chr('"');

  while (s < e)
  {
    int c = *s;

    if ((c & 0x80) == 0)
    {
      if (c == '"')
      {
        str(t, s - t);
        t = s + 1;
        str("\"\"", 2);
      }
      else if ((c < 0x20 && c != '\t') || c == '\\')
      {
        str(t, s - t);
        t = s + 1;

        switch (c)
        {
          case '\b':
            c = 'b';
            break;

          case '\f':
            c = 'f';
            break;

          case '\n':
            c = 'n';
            break;

          case '\r':
            c = 'r';
            break;

          case '\t':
            c = 't';
            break;
        }

        if (c > 0x20)
        {
          chr('\\');
          chr(c);
        }
        else
        {
          str("\\x", 2);
          hex(c, 2);
        }
      }
    }
    ++s;
  }
  str(t, s - t);

  chr('"');
}

// output in hex
void Output::hex(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;

  if (s < e)
  {
    hex(static_cast<uint8_t>(*s++), 2);
    while (s < e)
    {
      chr(' ');
      hex(static_cast<uint8_t>(*s++), 2);
    }
  }
}

// output quoted string in JSON
void Output::json(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;
  const char *t = s;

  chr('"');

  while (s < e)
  {
    int c = *s;

    if ((c & 0x80) == 0)
    {
      if (c < 0x20 || c == '"' || c == '\\')
      {
        str(t, s - t);
        t = s + 1;

        switch (c)
        {
          case '\b':
            c = 'b';
            break;

          case '\f':
            c = 'f';
            break;

          case '\n':
            c = 'n';
            break;

          case '\r':
            c = 'r';
            break;

          case '\t':
            c = 't';
            break;
        }

        if (c > 0x20)
        {
          chr('\\');
          chr(c);
        }
        else
        {
          str("\\u", 2);
          hex(c, 4);
        }
      }
    }
    ++s;
  }
  str(t, s - t);

  chr('"');
}

// output in XML
void Output::xml(const char *data, size_t size)
{
  const char *s = data;
  const char *e = data + size;
  const char *t = s;

  while (s < e)
  {
    int c = *s;

    if ((c & 0x80) == 0)
    {
      switch (c)
      {
        case '&':
          str(t, s - t);
          t = s + 1;
          str("&amp;", 5);
          break;

        case '<':
          str(t, s - t);
          t = s + 1;
          str("&lt;", 4);
          break;

        case '>':
          str(t, s - t);
          t = s + 1;
          str("&gt;", 4);
          break;

        case '"':
          str(t, s - t);
          t = s + 1;
          str("&quot;", 6);
          break;

        case 0x7f:
          str(t, s - t);
          t = s + 1;
          str("&#x7f;", 6);
          break;

        default:
          if (c < 0x20)
          {
            str(t, s - t);
            t = s + 1;
            str("&#", 2);
            num(c);
            chr(';');
          }
      }
    }
    ++s;
  }
  str(t, s - t);
}

// flush a block of data as truncated lines limited to --width columns, taking into account tabs, UTF-8, ANSI
bool Output::flush_truncated_lines(const char *data, size_t size)
{
  if (skip_)
  {
    const char *end = static_cast<const char*>(memchr(data, '\n', size));

    if (end == NULL)
      return false;

    size_t num = end - data + 1;

    data += num;
    size -= num;

    skip_ = false;
  }

  while (size > 0)
  {
    // search for \n or cols_ + width Unicode characters, whichever comes first
    const char *end = data + size;
    const char *esc = end;
    const char *scan;

    for (scan = data; scan < end && cols_ <= flag_width && *scan != '\n'; ++scan)
    {
      if (ansi_ != ANSI::NA)
      {
        // skip ANSI escape sequence state machine
        switch (ansi_)
        {
          case ANSI::ESC:
            switch (*scan)
            {
              case '[':
                // CSI \e[... sequence
                ansi_ = ANSI::CSI;
                break;

              case ']':
                // OSC \e]...BEL|ST sequence
                ansi_ = ANSI::OSC;
                break;

              default:
                ansi_ = ANSI::NA;
            }
            break;

          case ANSI::CSI:
            if (*scan >= 0x40 && *scan <= 0x7e)
              ansi_ = ANSI::NA;
            break;

          case ANSI::OSC:
            if (*scan == '\a')
              ansi_ = ANSI::NA;
            else if (*scan == '\033')
              ansi_ = ANSI::OSC_ESC;
            break;

          case ANSI::OSC_ESC:
            if (*scan == '\\')
              ansi_ = ANSI::NA;
            else
              ansi_ = ANSI::OSC;
            break;

          default:
            ansi_ = ANSI::NA;
        }
      }
      else if (*scan == '\t')
      {
        cols_ += 1 + (~cols_ & 7);
      }
      else if (*scan == '\033')
      {
        esc = scan;
        ansi_ = ANSI::ESC;
      }
      else if (static_cast<unsigned char>(*scan) < 0x80)
      {
        cols_ += (*scan >= ' ');
      }
      else
      {
        cols_ += (*scan & 0xc0) != 0x80;
      }
    }

    if (scan < end && *scan == '\n')
    {
      // write data with newline
      size_t num = scan - data + 1;
      size_t nwritten = fwrite(data, 1, num, file);
      if (nwritten < num)
        return true;

      data += num;
      size -= num;

      cols_ = 0;
    }
    else if (cols_ <= flag_width)
    {
      // write data
      size_t num = scan - data;
      size_t nwritten = fwrite(data, 1, num, file);
      if (nwritten < num)
        return true;

      data += num;
      size -= num;
    }
    else
    {
      // counted one column over, back up to ensure UTF-8 multibyte sequence is complete
      --scan;

      size_t num = scan - data;

      if (ansi_ != ANSI::NA)
      {
        // write data up to but not including ANSI ESC
        size_t len = esc - data;
        size_t nwritten = fwrite(data, 1, len, file);
        if (nwritten < len)
          return true;
      }
      else
      {
        // write data
        size_t nwritten = fwrite(data, 1, num, file);
        if (nwritten < num)
          return true;
      }

      data += num;
      size -= num;

      // disable CSI when line was truncated
      if (flag_color != NULL)
        if (fwrite("\033[m", 1, 3, file) < 3)
          return true;

      // write newline
#ifdef OS_WIN
      if (fwrite("\r\n", 1, 2, file) < 2)
        return true;
#else
      if (fwrite("\n", 1, 1, file) < 1)
        return true;
#endif

      cols_ = 0;

      // look for newline to reach next line
      scan = static_cast<const char*>(memchr(data, '\n', size));
      if (scan == NULL)
      {
        skip_ = true;
        break;
      }

      // move to next line
      num = scan - data + 1;
      data += num;
      size -= num;
    }
  }

  return false;
}

const char *Output::Dump::color_hex[4] = { match_ms, color_sl, match_mc, color_cx };

const char *Output::Tree::bar = "|  ";
const char *Output::Tree::ptr = "|_ ";
const char *Output::Tree::end = "~  ";

std::string Output::Tree::path;
int Output::Tree::depth;
