/* TUI windows implemented in Python

   Copyright (C) 2020-2022 Free Software Foundation, Inc.

   This file is part of GDB.

   This program 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 of the License, or
   (at your option) any later version.

   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.  */


#include "defs.h"
#include "arch-utils.h"
#include "python-internal.h"

#ifdef TUI

/* Note that Python's public headers may define HAVE_NCURSES_H, so if
   we unconditionally include this (outside the #ifdef above), then we
   can get a compile error when ncurses is not in fact installed.  See
   PR tui/25597; or the upstream Python bug
   https://bugs.python.org/issue20768.  */
#include "gdb_curses.h"

#include "tui/tui-data.h"
#include "tui/tui-io.h"
#include "tui/tui-layout.h"
#include "tui/tui-wingeneral.h"
#include "tui/tui-winsource.h"

class tui_py_window;

/* A PyObject representing a TUI window.  */

struct gdbpy_tui_window
{
  PyObject_HEAD

  /* The TUI window, or nullptr if the window has been deleted.  */
  tui_py_window *window;

  /* Return true if this object is valid.  */
  bool is_valid () const;
};

extern PyTypeObject gdbpy_tui_window_object_type
    CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");

/* A TUI window written in Python.  */

class tui_py_window : public tui_win_info
{
public:

  tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper)
    : m_name (name),
      m_wrapper (std::move (wrapper))
  {
    m_wrapper->window = this;
  }

  ~tui_py_window ();

  DISABLE_COPY_AND_ASSIGN (tui_py_window);

  /* Set the "user window" to the indicated reference.  The user
     window is the object returned the by user-defined window
     constructor.  */
  void set_user_window (gdbpy_ref<> &&user_window)
  {
    m_window = std::move (user_window);
  }

  const char *name () const override
  {
    return m_name.c_str ();
  }

  void rerender () override;
  void do_scroll_vertical (int num_to_scroll) override;
  void do_scroll_horizontal (int num_to_scroll) override;

  void refresh_window () override
  {
    if (m_inner_window != nullptr)
      {
	wnoutrefresh (handle.get ());
	touchwin (m_inner_window.get ());
	tui_wrefresh (m_inner_window.get ());
      }
    else
      tui_win_info::refresh_window ();
  }

  void click (int mouse_x, int mouse_y, int mouse_button) override;

  /* Erase and re-box the window.  */
  void erase ()
  {
    if (is_visible () && m_inner_window != nullptr)
      {
	werase (m_inner_window.get ());
	check_and_display_highlight_if_needed ();
      }
  }

  /* Write STR to the window.  FULL_WINDOW is true to erase the window
     contents beforehand.  */
  void output (const char *str, bool full_window);

  /* A helper function to compute the viewport width.  */
  int viewport_width () const
  {
    return std::max (0, width - 2);
  }

  /* A helper function to compute the viewport height.  */
  int viewport_height () const
  {
    return std::max (0, height - 2);
  }

private:

  /* The name of this window.  */
  std::string m_name;

  /* We make our own inner window, so that it is easy to print without
     overwriting the border.  */
  std::unique_ptr<WINDOW, curses_deleter> m_inner_window;

  /* The underlying Python window object.  */
  gdbpy_ref<> m_window;

  /* The Python wrapper for this object.  */
  gdbpy_ref<gdbpy_tui_window> m_wrapper;
};

/* See gdbpy_tui_window declaration above.  */

bool
gdbpy_tui_window::is_valid () const
{
  return window != nullptr && tui_active;
}

tui_py_window::~tui_py_window ()
{
  gdbpy_enter enter_py (get_current_arch (), current_language);

  /* This can be null if the user-provided Python construction
     function failed.  */
  if (m_window != nullptr
      && PyObject_HasAttrString (m_window.get (), "close"))
    {
      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "close",
					       nullptr));
      if (result == nullptr)
	gdbpy_print_stack ();
    }

  /* Unlink.  */
  m_wrapper->window = nullptr;
  /* Explicitly free the Python references.  We have to do this
     manually because we need to hold the GIL while doing so.  */
  m_wrapper.reset (nullptr);
  m_window.reset (nullptr);
}

void
tui_py_window::rerender ()
{
  tui_win_info::rerender ();

  gdbpy_enter enter_py (get_current_arch (), current_language);

  int h = viewport_height ();
  int w = viewport_width ();
  if (h == 0 || w == 0)
    {
      /* The window would be too small, so just remove the
	 contents.  */
      m_inner_window.reset (nullptr);
      return;
    }
  m_inner_window.reset (newwin (h, w, y + 1, x + 1));

  if (PyObject_HasAttrString (m_window.get (), "render"))
    {
      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "render",
					       nullptr));
      if (result == nullptr)
	gdbpy_print_stack ();
    }
}

void
tui_py_window::do_scroll_horizontal (int num_to_scroll)
{
  gdbpy_enter enter_py (get_current_arch (), current_language);

  if (PyObject_HasAttrString (m_window.get (), "hscroll"))
    {
      gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll",
					       "i", num_to_scroll, nullptr));
      if (result == nullptr)
	gdbpy_print_stack ();
    }
}

void
tui_py_window::do_scroll_vertical (int num_to_scroll)
{
  gdbpy_enter enter_py (get_current_arch (), current_language);

  if (PyObject_HasAttrString (m_window.get (), "vscroll"))
    {
      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll",
					       "i", num_to_scroll, nullptr));
      if (result == nullptr)
	gdbpy_print_stack ();
    }
}

void
tui_py_window::click (int mouse_x, int mouse_y, int mouse_button)
{
  gdbpy_enter enter_py (get_current_arch (), current_language);

  if (PyObject_HasAttrString (m_window.get (), "click"))
    {
      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "click",
					       "iii", mouse_x, mouse_y,
					       mouse_button));
      if (result == nullptr)
	gdbpy_print_stack ();
    }
}

void
tui_py_window::output (const char *text, bool full_window)
{
  if (m_inner_window != nullptr)
    {
      if (full_window)
	werase (m_inner_window.get ());

      tui_puts (text, m_inner_window.get ());
      if (full_window)
	check_and_display_highlight_if_needed ();
      else
	tui_wrefresh (m_inner_window.get ());
    }
}



/* A callable that is used to create a TUI window.  It wraps the
   user-supplied window constructor.  */

class gdbpy_tui_window_maker
{
public:

  explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr)
    : m_constr (std::move (constr))
  {
  }

  ~gdbpy_tui_window_maker ();

  gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other) noexcept
    : m_constr (std::move (other.m_constr))
  {
  }

  gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other)
  {
    gdbpy_enter enter_py (get_current_arch (), current_language);
    m_constr = other.m_constr;
  }

  gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other)
  {
    m_constr = std::move (other.m_constr);
    return *this;
  }

  gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other)
  {
    gdbpy_enter enter_py (get_current_arch (), current_language);
    m_constr = other.m_constr;
    return *this;
  }

  tui_win_info *operator() (const char *name);

private:

  /* A constructor that is called to make a TUI window.  */
  gdbpy_ref<> m_constr;
};

gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
{
  gdbpy_enter enter_py (get_current_arch (), current_language);
  m_constr.reset (nullptr);
}

tui_win_info *
gdbpy_tui_window_maker::operator() (const char *win_name)
{
  gdbpy_enter enter_py (get_current_arch (), current_language);

  gdbpy_ref<gdbpy_tui_window> wrapper
    (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type));
  if (wrapper == nullptr)
    {
      gdbpy_print_stack ();
      return nullptr;
    }

  std::unique_ptr<tui_py_window> window
    (new tui_py_window (win_name, wrapper));

  gdbpy_ref<> user_window
    (PyObject_CallFunctionObjArgs (m_constr.get (),
				   (PyObject *) wrapper.get (),
				   nullptr));
  if (user_window == nullptr)
    {
      gdbpy_print_stack ();
      return nullptr;
    }

  window->set_user_window (std::move (user_window));
  /* Window is now owned by the TUI.  */
  return window.release ();
}

/* Implement "gdb.register_window_type".  */

PyObject *
gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw)
{
  static const char *keywords[] = { "name", "constructor", nullptr };

  const char *name;
  PyObject *cons_obj;

  if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords,
					&name, &cons_obj))
    return nullptr;

  try
    {
      gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj));
      tui_register_window (name, constr);
    }
  catch (const gdb_exception &except)
    {
      gdbpy_convert_exception (except);
      return nullptr;
    }

  Py_RETURN_NONE;
}



/* Require that "Window" be a valid window.  */

#define REQUIRE_WINDOW(Window)					\
    do {							\
      if (!(Window)->is_valid ())				\
	return PyErr_Format (PyExc_RuntimeError,		\
			     _("TUI window is invalid."));	\
    } while (0)

/* Require that "Window" be a valid window.  */

#define REQUIRE_WINDOW_FOR_SETTER(Window)			\
    do {							\
      if (!(Window)->is_valid ())				\
	{							\
	  PyErr_Format (PyExc_RuntimeError,			\
			_("TUI window is invalid."));		\
	  return -1;						\
	}							\
    } while (0)

/* Python function which checks the validity of a TUI window
   object.  */
static PyObject *
gdbpy_tui_is_valid (PyObject *self, PyObject *args)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;

  if (win->is_valid ())
    Py_RETURN_TRUE;
  Py_RETURN_FALSE;
}

/* Python function that erases the TUI window.  */
static PyObject *
gdbpy_tui_erase (PyObject *self, PyObject *args)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;

  REQUIRE_WINDOW (win);

  win->window->erase ();

  Py_RETURN_NONE;
}

/* Python function that writes some text to a TUI window.  */
static PyObject *
gdbpy_tui_write (PyObject *self, PyObject *args)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
  const char *text;
  int full_window = 0;

  if (!PyArg_ParseTuple (args, "s|i", &text, &full_window))
    return nullptr;

  REQUIRE_WINDOW (win);

  win->window->output (text, full_window);

  Py_RETURN_NONE;
}

/* Return the width of the TUI window.  */
static PyObject *
gdbpy_tui_width (PyObject *self, void *closure)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
  REQUIRE_WINDOW (win);
  gdbpy_ref<> result
    = gdb_py_object_from_longest (win->window->viewport_width ());
  return result.release ();
}

/* Return the height of the TUI window.  */
static PyObject *
gdbpy_tui_height (PyObject *self, void *closure)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
  REQUIRE_WINDOW (win);
  gdbpy_ref<> result
    = gdb_py_object_from_longest (win->window->viewport_height ());
  return result.release ();
}

/* Return the title of the TUI window.  */
static PyObject *
gdbpy_tui_title (PyObject *self, void *closure)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
  REQUIRE_WINDOW (win);
  return host_string_to_python_string (win->window->title.c_str ()).release ();
}

/* Set the title of the TUI window.  */
static int
gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure)
{
  gdbpy_tui_window *win = (gdbpy_tui_window *) self;

  REQUIRE_WINDOW_FOR_SETTER (win);

  if (newvalue == nullptr)
    {
      PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute."));
      return -1;
    }

  gdb::unique_xmalloc_ptr<char> value
    = python_string_to_host_string (newvalue);
  if (value == nullptr)
    return -1;

  win->window->title = value.get ();
  return 0;
}

static gdb_PyGetSetDef tui_object_getset[] =
{
  { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL },
  { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL },
  { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.",
    NULL },
  { NULL }  /* Sentinel */
};

static PyMethodDef tui_object_methods[] =
{
  { "is_valid", gdbpy_tui_is_valid, METH_NOARGS,
    "is_valid () -> Boolean\n\
Return true if this TUI window is valid, false if not." },
  { "erase", gdbpy_tui_erase, METH_NOARGS,
    "Erase the TUI window." },
  { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS,
    "Append a string to the TUI window." },
  { NULL } /* Sentinel.  */
};

PyTypeObject gdbpy_tui_window_object_type =
{
  PyVarObject_HEAD_INIT (NULL, 0)
  "gdb.TuiWindow",		  /*tp_name*/
  sizeof (gdbpy_tui_window),	  /*tp_basicsize*/
  0,				  /*tp_itemsize*/
  0,				  /*tp_dealloc*/
  0,				  /*tp_print*/
  0,				  /*tp_getattr*/
  0,				  /*tp_setattr*/
  0,				  /*tp_compare*/
  0,				  /*tp_repr*/
  0,				  /*tp_as_number*/
  0,				  /*tp_as_sequence*/
  0,				  /*tp_as_mapping*/
  0,				  /*tp_hash */
  0,				  /*tp_call*/
  0,				  /*tp_str*/
  0,				  /*tp_getattro*/
  0,				  /*tp_setattro */
  0,				  /*tp_as_buffer*/
  Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,  /*tp_flags*/
  "GDB TUI window object",	  /* tp_doc */
  0,				  /* tp_traverse */
  0,				  /* tp_clear */
  0,				  /* tp_richcompare */
  0,				  /* tp_weaklistoffset */
  0,				  /* tp_iter */
  0,				  /* tp_iternext */
  tui_object_methods,		  /* tp_methods */
  0,				  /* tp_members */
  tui_object_getset,		  /* tp_getset */
  0,				  /* tp_base */
  0,				  /* tp_dict */
  0,				  /* tp_descr_get */
  0,				  /* tp_descr_set */
  0,				  /* tp_dictoffset */
  0,				  /* tp_init */
  0,				  /* tp_alloc */
};

#endif /* TUI */

/* Initialize this module.  */

int
gdbpy_initialize_tui ()
{
#ifdef TUI
  gdbpy_tui_window_object_type.tp_new = PyType_GenericNew;
  if (PyType_Ready (&gdbpy_tui_window_object_type) < 0)
    return -1;
#endif	/* TUI */

  return 0;
}
