/* Canvas for random-access procedural text art.
   Copyright (C) 2023-2024 Free Software Foundation, Inc.
   Contributed by David Malcolm <dmalcolm@redhat.com>.

This file is part of GCC.

GCC is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.

GCC is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3.  If not see
<http://www.gnu.org/licenses/>.  */

#include "config.h"
#define INCLUDE_VECTOR
#include "system.h"
#include "coretypes.h"
#include "pretty-print.h"
#include "selftest.h"
#include "text-art/selftests.h"
#include "text-art/canvas.h"

using namespace text_art;

canvas::canvas (size_t size, const style_manager &style_mgr)
: m_cells (size_t (size.w, size.h)),
  m_style_mgr (style_mgr)
{
  m_cells.fill (cell_t (' '));
}

void
canvas::paint (coord_t coord, styled_unichar ch)
{
  m_cells.set (coord, std::move (ch));
}

void
canvas::paint_text (coord_t coord, const styled_string &text)
{
  for (auto ch : text)
    {
      paint (coord, ch);
      if (ch.double_width_p ())
	coord.x += 2;
      else
	coord.x++;
    }
}

void
canvas::fill (rect_t rect, cell_t c)
{
  for (int y = rect.get_min_y (); y < rect.get_next_y (); y++)
    for (int x = rect.get_min_x (); x < rect.get_next_x (); x++)
      paint(coord_t (x, y), c);
}

void
canvas::debug_fill ()
{
  fill (rect_t (coord_t (0, 0), get_size ()), cell_t ('*'));
}

void
canvas::print_to_pp (pretty_printer *pp,
		     const char *per_line_prefix) const
{
  for (int y = 0; y < m_cells.get_size ().h; y++)
    {
      style::id_t curr_style_id = 0;
      if (per_line_prefix)
	pp_string (pp, per_line_prefix);

      pretty_printer line_pp;
      line_pp.show_color = pp->show_color;
      line_pp.url_format = pp->url_format;
      const int final_x_in_row = get_final_x_in_row (y);
      for (int x = 0; x <= final_x_in_row; x++)
	{
	  if (x > 0)
	    {
	      const cell_t prev_cell = m_cells.get (coord_t (x - 1, y));
	      if (prev_cell.double_width_p ())
		 /* This cell is just a placeholder for the
		    2nd column of a double width cell; skip it.  */
		continue;
	    }
	  const cell_t cell = m_cells.get (coord_t (x, y));
	  if (cell.get_style_id () != curr_style_id)
	    {
	      m_style_mgr.print_any_style_changes (&line_pp,
						   curr_style_id,
						   cell.get_style_id ());
	      curr_style_id = cell.get_style_id ();
	    }
	  pp_unicode_character (&line_pp, cell.get_code ());
	  if (cell.emoji_variant_p ())
	    /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji
	       variation of the char.  */
	    pp_unicode_character (&line_pp, 0xFE0F);
	}
      /* Reset the style at the end of each line.  */
      m_style_mgr.print_any_style_changes (&line_pp, curr_style_id, 0);

      /* Print from line_pp to pp, stripping trailing whitespace from
	 the line.  */
      const char *line_buf = pp_formatted_text (&line_pp);
      ::size_t len = strlen (line_buf);
      while (len > 0)
	{
	  if (line_buf[len - 1] == ' ')
	    len--;
	  else
	    break;
	}
      pp_append_text (pp, line_buf, line_buf + len);
      pp_newline (pp);
    }
}

DEBUG_FUNCTION void
canvas::debug (bool styled) const
{
  pretty_printer pp;
  if (styled)
    {
      pp_show_color (&pp) = true;
      pp.url_format = determine_url_format (DIAGNOSTICS_URL_AUTO);
    }
  print_to_pp (&pp);
  fprintf (stderr, "%s\n", pp_formatted_text (&pp));
}

/* Find right-most non-default cell in this row,
   or -1 if all are default.  */

int
canvas::get_final_x_in_row (int y) const
{
  for (int x = m_cells.get_size ().w - 1; x >= 0; x--)
    {
      cell_t cell = m_cells.get (coord_t (x, y));
      if (cell.get_code () != ' '
	  || cell.get_style_id () != style::id_plain)
	return x;
    }
  return -1;
}

#if CHECKING_P

namespace selftest {

static void
test_blank ()
{
  style_manager sm;
  canvas c (canvas::size_t (5, 5), sm);
  ASSERT_CANVAS_STREQ (c, false,
		       ("\n"
			"\n"
			"\n"
			"\n"
			"\n"));
}

static void
test_abc ()
{
  style_manager sm;
  canvas c (canvas::size_t (3, 3), sm);
  c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
  c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
  c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));

  ASSERT_CANVAS_STREQ (c, false,
		       "A\n B\n  C\n");
}

static void
test_debug_fill ()
{
  style_manager sm;
  canvas c (canvas::size_t (5, 3), sm);
  c.debug_fill();
  ASSERT_CANVAS_STREQ (c, false,
		       ("*****\n"
			"*****\n"
			"*****\n"));
}

static void
test_text ()
{
  style_manager sm;
  canvas c (canvas::size_t (6, 1), sm);
  c.paint_text (canvas::coord_t (0, 0), styled_string (sm, "012345"));
  ASSERT_CANVAS_STREQ (c, false,
		       ("012345\n"));

  /* Paint an emoji character that should occupy two canvas columns when
     printed.  */
  c.paint_text (canvas::coord_t (2, 0), styled_string ((cppchar_t)0x1f642));
  ASSERT_CANVAS_STREQ (c, false,
		       ("01🙂45\n"));
}

static void
test_circle ()
{
  canvas::size_t sz (30, 30);
  style_manager sm;
  canvas canvas (sz, sm);
  canvas::coord_t center (sz.w / 2, sz.h / 2);
  const int radius = 12;
  const int radius_squared = radius * radius;
  for (int x = 0; x < sz.w; x++)
    for (int y = 0; y < sz.h; y++)
      {
	int dx = x - center.x;
	int dy = y - center.y;
	char ch = "AB"[(x + y) % 2];
	if (dx * dx + dy * dy < radius_squared)
	  canvas.paint (canvas::coord_t (x, y), styled_unichar (ch));
      }
  ASSERT_CANVAS_STREQ
    (canvas, false,
     ("\n"
      "\n"
      "\n"
      "\n"
      "           BABABABAB\n"
      "         ABABABABABABA\n"
      "        ABABABABABABABA\n"
      "       ABABABABABABABABA\n"
      "      ABABABABABABABABABA\n"
      "     ABABABABABABABABABABA\n"
      "     BABABABABABABABABABAB\n"
      "    BABABABABABABABABABABAB\n"
      "    ABABABABABABABABABABABA\n"
      "    BABABABABABABABABABABAB\n"
      "    ABABABABABABABABABABABA\n"
      "    BABABABABABABABABABABAB\n"
      "    ABABABABABABABABABABABA\n"
      "    BABABABABABABABABABABAB\n"
      "    ABABABABABABABABABABABA\n"
      "    BABABABABABABABABABABAB\n"
      "     BABABABABABABABABABAB\n"
      "     ABABABABABABABABABABA\n"
      "      ABABABABABABABABABA\n"
      "       ABABABABABABABABA\n"
      "        ABABABABABABABA\n"
      "         ABABABABABABA\n"
      "           BABABABAB\n"
      "\n"
      "\n"
      "\n"));
}

static void
test_color_circle ()
{
  const canvas::size_t sz (10, 10);
  const canvas::coord_t center (sz.w / 2, sz.h / 2);
  const int outer_r2 = 25;
  const int inner_r2 = 10;
  style_manager sm;
  canvas c (sz, sm);
  for (int x = 0; x < sz.w; x++)
    for (int y = 0; y < sz.h; y++)
      {
	const int dist_from_center_squared
	  = ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
	if (dist_from_center_squared < outer_r2)
	  {
	    style s;
	    if (dist_from_center_squared < inner_r2)
	      s.m_fg_color = style::named_color::RED;
	    else
	      s.m_fg_color = style::named_color::GREEN;
	    c.paint (canvas::coord_t (x, y),
		     styled_unichar ('*', false, sm.get_or_create_id (s)));
	  }
      }
  ASSERT_EQ (sm.get_num_styles (), 3);
  ASSERT_CANVAS_STREQ
    (c, false,
     ("\n"
      "   *****\n"
      "  *******\n"
      " *********\n"
      " *********\n"
      " *********\n"
      " *********\n"
      " *********\n"
      "  *******\n"
      "   *****\n"));
  ASSERT_CANVAS_STREQ
    (c, true,
     ("\n"
      "   *****\n"
      "  *******\n"
      " *********\n"
      " *********\n"
      " *********\n"
      " *********\n"
      " *********\n"
      "  *******\n"
      "   *****\n"));
}

static void
test_bold ()
{
  auto_fix_quotes fix_quotes;
  style_manager sm;
  styled_string s (styled_string::from_fmt (sm, nullptr,
					    "before %qs after", "foo"));
  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
  c.paint_text (canvas::coord_t (0, 0), s);
  ASSERT_CANVAS_STREQ (c, false,
		       "before `foo' after\n");
  ASSERT_CANVAS_STREQ (c, true,
		       "before `foo' after\n");
}

static void
test_emoji ()
{
  style_manager sm;
  styled_string s (0x26A0, /* U+26A0 WARNING SIGN.  */
		   true);
  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
  c.paint_text (canvas::coord_t (0, 0), s);
  ASSERT_CANVAS_STREQ (c, false, "⚠️\n");
  ASSERT_CANVAS_STREQ (c, true, "⚠️\n");
}

static void
test_emoji_2 ()
{
  style_manager sm;
  styled_string s;
  s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
			   true));
  s.append (styled_string (sm, "test"));
  ASSERT_EQ (s.size (), 5);
  ASSERT_EQ (s.calc_canvas_width (), 5);
  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
  c.paint_text (canvas::coord_t (0, 0), s);
  ASSERT_CANVAS_STREQ (c, false,
		       /* U+26A0 WARNING SIGN, as UTF-8: 0xE2 0x9A 0xA0.  */
		       "\xE2\x9A\xA0"
		       /* U+FE0F VARIATION SELECTOR-16, as UTF-8: 0xEF 0xB8 0x8F.  */
		       "\xEF\xB8\x8F"
		       "test\n");
}

static void
test_canvas_urls ()
{
  style_manager sm;
  canvas canvas (canvas::size_t (9, 3), sm);
  styled_string foo_ss (sm, "foo");
  foo_ss.set_url (sm, "https://www.example.com/foo");
  styled_string bar_ss (sm, "bar");
  bar_ss.set_url (sm, "https://www.example.com/bar");
  canvas.paint_text(canvas::coord_t (1, 1), foo_ss);
  canvas.paint_text(canvas::coord_t (5, 1), bar_ss);

  ASSERT_CANVAS_STREQ (canvas, false,
		       ("\n"
			" foo bar\n"
			"\n"));
  {
    pretty_printer pp;
    pp_show_color (&pp) = true;
    pp.url_format = URL_FORMAT_ST;
    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
			 (/* Line 1.  */
			  "\n"
			  /* Line 2.  */
			  " "
			  "\33]8;;https://www.example.com/foo\33\\foo\33]8;;\33\\"
			  " "
			  "\33]8;;https://www.example.com/bar\33\\bar\33]8;;\33\\"
			  "\n"
			  /* Line 3.  */
			  "\n"));
  }

  {
    pretty_printer pp;
    pp_show_color (&pp) = true;
    pp.url_format = URL_FORMAT_BEL;
    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
			 (/* Line 1.  */
			  "\n"
			  /* Line 2.  */
			  " "
			  "\33]8;;https://www.example.com/foo\afoo\33]8;;\a"
			  " "
			  "\33]8;;https://www.example.com/bar\abar\33]8;;\a"
			  "\n"
			  /* Line 3.  */
			  "\n"));
  }
}

/* Run all selftests in this file.  */

void
text_art_canvas_cc_tests ()
{
  test_blank ();
  test_abc ();
  test_debug_fill ();
  test_text ();
  test_circle ();
  test_color_circle ();
  test_bold ();
  test_emoji ();
  test_emoji_2 ();
  test_canvas_urls ();
}

} // namespace selftest


#endif /* #if CHECKING_P */