/*      $NetBSD: pr_comment.c,v 1.174 2025/01/04 10:28:08 rillig Exp $  */

/*-
* SPDX-License-Identifier: BSD-4-Clause
*
* Copyright (c) 1985 Sun Microsystems, Inc.
* Copyright (c) 1980, 1993
*      The Regents of the University of California.  All rights reserved.
* 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. All advertising materials mentioning features or use of this software
*    must display the following acknowledgement:
*      This product includes software developed by the University of
*      California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
*    may be used to endorse or promote products derived from this software
*    without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS OR CONTRIBUTORS 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.
*/

#include <sys/cdefs.h>
__RCSID("$NetBSD: pr_comment.c,v 1.174 2025/01/04 10:28:08 rillig Exp $");

#include <string.h>

#include "indent.h"

static void
com_add_char(char ch)
{
       buf_add_char(&com, ch);
}

static void
com_add_star(void)
{
       if (opt.star_comment_cont)
               buf_add_str(&com, " * ");
}

static bool
fits_in_one_line(int max_line_length)
{
       for (const char *start = in.p, *p = start; *p != '\n'; p++) {
               if (p[0] == '*' && p[1] == '/') {
                       while (p - in.p >= 2
                           && ch_isblank(p[-1])
                           && ch_isblank(p[-2]))
                               p--;
                       int ind = ind_add(ps.comment_ind + 3,
                           start, (size_t)(p - start));
                       ind += p == start || ch_isblank(p[-1]) ? 2 : 3;
                       return ind <= max_line_length;
               }
       }
       return false;
}

static bool
is_block_comment(void)
{
       const char *p = in.p;
       while (*p == '*')
               p++;
       return *p == '\n';
}

static void
analyze_comment(bool *p_may_wrap, bool *p_delim, int *p_line_length)
{
       bool may_wrap = true;
       bool delim = false;     // only relevant if may_wrap
       int ind;
       int line_length = opt.max_line_length;

       if (in.p - in.line.s == 2 && !opt.format_col1_comments) {
               may_wrap = false;
               ind = 0;
       } else {
               if (in.p[0] == '-' || in.p[0] == '*' ||
                   token.s[token.len - 1] == '/' ||
                   (in.p[0] == '\n' && !opt.format_block_comments))
                       may_wrap = false;

               if (com.len > 0)
                       output_line();
               if (lab.len == 0 && code.len == 0) {
                       if (is_block_comment())
                               out.line_kind = lk_block_comment;
                       ind = (ps.ind_level - opt.unindent_displace)
                           * opt.indent_size;
                       if (ind <= 0)
                               ind = opt.format_col1_comments ? 0 : 1;
                       line_length = opt.block_comment_max_line_length;
                       if (may_wrap && in.p[0] == '\n')
                               delim = true;
                       if (may_wrap && opt.comment_delimiter_on_blank_line)
                               delim = true;
               } else {
                       int min_ind = code.len > 0
                           ? ind_add(compute_code_indent(), code.s, code.len)
                           : ind_add(compute_label_indent(), lab.s, lab.len);

                       ind = ps.line_has_decl || ps.ind_level == 0
                           ? opt.decl_comment_column - 1
                           : opt.comment_column - 1;
                       if (ind <= min_ind)
                               ind = next_tab(min_ind);
                       if (ind + 25 > line_length)
                               line_length = ind + 25;
               }
       }

       if (!may_wrap) {
               /* Find out how much indentation there was originally, because
                * that much will have to be ignored by output_line. */
               size_t len = (size_t)(in.p - 2 - in.line.s);
               ps.comment_shift = -ind_add(0, in.line.s, len);
       } else {
               ps.comment_shift = 0;
               if (!(in.p[0] == '\t' && !ch_isblank(in.p[1])))
                       while (ch_isblank(in.p[0]))
                               in.p++;
       }

       ps.comment_ind = ind;
       *p_may_wrap = may_wrap;
       *p_delim = delim;
       *p_line_length = line_length;
}

static void
copy_comment_start(bool may_wrap, bool *delim, int line_length)
{
       ps.comment_cont = false;
       buf_add_chars(&com, token.s, token.len);        // "/*" or "//"

       if (may_wrap) {
               if (!ch_isblank(in.p[0]))
                       com_add_char(' ');

               if (*delim && fits_in_one_line(line_length))
                       *delim = false;
               if (*delim) {
                       output_line();
                       com_add_star();
               }
       }
}

static void
copy_comment_wrap_text(int line_length, ssize_t *last_blank)
{
       int ind = ind_add(ps.comment_ind, com.s, com.len);
       for (;;) {
               char ch = inp_next();
               if (ch_isblank(ch))
                       *last_blank = (ssize_t)com.len;
               com_add_char(ch);
               ind++;
               if (memchr("*\n\r\t", in.p[0], 5) != NULL)
                       break;
               if (ind >= line_length && *last_blank != -1)
                       break;
       }

       if (ind <= line_length)
               return;
       if (ch_isspace(com.s[com.len - 1]))
               return;

       if (*last_blank == -1) {        /* only a single word in this line */
               output_line();
               com_add_star();
               return;
       }

       // Move the overlong word to the next line.
       const char *last_word = com.s + *last_blank + 1;
       size_t last_word_len = com.len - (size_t)(*last_blank + 1);
       com.len = (size_t)*last_blank;
       buf_terminate(&com);
       output_line();
       com_add_star();

       /* Assume that output_line and com_add_delim left the "unused" part of
        * the now truncated buffer beyond com.s + com.len as-is. */
       memmove(com.s + com.len, last_word, last_word_len);
       com.len += last_word_len;
       buf_terminate(&com);
       *last_blank = -1;
}

/* In a comment that is re-wrapped, handle a single newline character. */
static bool
copy_comment_wrap_newline(ssize_t *last_blank, bool seen_newline)
{
       *last_blank = -1;
       if (seen_newline) {
               if (com.len > 3) {
                       output_line();
                       com_add_star();
               }
               output_line();
               com_add_star();
       } else {
               if (!(com.len > 0 && ch_isblank(com.s[com.len - 1])))
                       com_add_char(' ');
               *last_blank = (int)com.len - 1;
       }
       in.token_end_line++;

       /* flush any blanks and/or tabs at start of next line */
       inp_skip();             /* '\n' */
       while (ch_isblank(in.p[0]))
               in.p++;
       if (in.p[0] == '*' && in.p[1] == '/')
               return false;
       if (in.p[0] == '*') {
               in.p++;
               while (ch_isblank(in.p[0]))
                       in.p++;
       }

       return true;
}

static void
copy_comment_wrap_finish(int line_length, bool delim)
{
       if (delim) {
               if (com.len > 3)
                       output_line();
               buf_clear(&com);
       } else {
               size_t len = com.len;
               // XXX: This loop differs from the one below.
               while (ch_isblank(com.s[len - 1]))
                       len--;
               if (ind_add(ps.comment_ind, com.s, len) + 3 > line_length)
                       output_line();
       }

       while (com.len >= 2
           && ch_isblank(com.s[com.len - 1])
           && ch_isblank(com.s[com.len - 2]))
               com.len--;
       buf_terminate(&com);

       in.p += 2;
       if (com.len > 0 && ch_isblank(com.s[com.len - 1]))
               buf_add_str(&com, "*/");
       else
               buf_add_str(&com, " */");
}

static void
copy_comment_wrap(int line_length, bool delim)
{
       ssize_t last_blank = -1;        /* index of the last blank in 'com' */
       bool seen_newline = false;

       for (;;) {
               if (in.p[0] == '\n') {
                       if (had_eof)
                               goto unterminated_comment;
                       if (!copy_comment_wrap_newline(&last_blank,
                               seen_newline))
                               break;
                       seen_newline = true;
               } else if (in.p[0] == '*' && in.p[1] == '/')
                       break;
               else {
                       copy_comment_wrap_text(line_length, &last_blank);
                       seen_newline = false;
               }
       }

       copy_comment_wrap_finish(line_length, delim);
       return;

unterminated_comment:
       in.token_start_line = in.token_end_line;
       diag(1, "Unterminated comment");
       output_line();
}

static void
copy_comment_nowrap(void)
{
       char kind = token.s[token.len - 1];

       for (;;) {
               if (in.p[0] == '\n') {
                       if (kind == '/')
                               return;

                       if (had_eof) {
                               in.token_start_line = in.token_end_line;
                               diag(1, "Unterminated comment");
                               output_line();
                               return;
                       }

                       output_line();
                       in.token_end_line++;
                       inp_skip();
                       continue;
               }

               if (kind == '*' && in.p[0] == '*' && in.p[1] == '/') {
                       com_add_char(*in.p++);
                       com_add_char(*in.p++);
                       return;
               }

               com_add_char(*in.p++);
       }
}

void
process_comment(void)
{
       bool may_wrap, delim;
       int line_length;

       analyze_comment(&may_wrap, &delim, &line_length);
       copy_comment_start(may_wrap, &delim, line_length);
       if (may_wrap)
               copy_comment_wrap(line_length, delim);
       else
               copy_comment_nowrap();
}