/* Communication module for Android terminals.

Copyright (C) 2023-2025 Free Software Foundation, Inc.

This file is part of GNU Emacs.

GNU Emacs 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.

GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */

#include <config.h>

#include "lisp.h"
#include "androidterm.h"
#include "android.h"
#include "blockinput.h"
#include "keyboard.h"
#include "menu.h"

#ifndef ANDROID_STUBIFY

#include <android/log.h>

/* Flag indicating whether or not a popup menu has been posted and not
   yet popped down.  */

static int popup_activated_flag;

/* Serial number used to identify which context menu events are
   associated with the context menu currently being displayed.  */

unsigned int current_menu_serial;

int
popup_activated (void)
{
  return popup_activated_flag;
}



/* Toolkit menu implementation.  */

/* Structure describing the EmacsContextMenu class.  */

struct android_emacs_context_menu
{
  jclass class;
  jmethodID create_context_menu;
  jmethodID add_item;
  jmethodID add_submenu;
  jmethodID add_pane;
  jmethodID parent;
  jmethodID display;
  jmethodID dismiss;
};

/* Identifiers associated with the EmacsContextMenu class.  */
static struct android_emacs_context_menu menu_class;

static void
android_init_emacs_context_menu (void)
{
  jclass old;

  menu_class.class
    = (*android_java_env)->FindClass (android_java_env,
				      "org/gnu/emacs/EmacsContextMenu");
  eassert (menu_class.class);

  old = menu_class.class;
  menu_class.class
    = (jclass) (*android_java_env)->NewGlobalRef (android_java_env,
						  (jobject) old);
  ANDROID_DELETE_LOCAL_REF (old);

  if (!menu_class.class)
    emacs_abort ();

#define FIND_METHOD(c_name, name, signature)			\
  menu_class.c_name						\
    = (*android_java_env)->GetMethodID (android_java_env,	\
					menu_class.class,	\
					name, signature);	\
  eassert (menu_class.c_name);

#define FIND_METHOD_STATIC(c_name, name, signature)		\
  menu_class.c_name						\
    = (*android_java_env)->GetStaticMethodID (android_java_env,	\
					      menu_class.class,	\
					      name, signature);	\
  eassert (menu_class.c_name);

  FIND_METHOD_STATIC (create_context_menu, "createContextMenu",
		      "(Ljava/lang/String;)"
		      "Lorg/gnu/emacs/EmacsContextMenu;");

  FIND_METHOD (add_item, "addItem", "(ILjava/lang/String;ZZZ"
	       "Ljava/lang/String;Z)V");
  FIND_METHOD (add_submenu, "addSubmenu", "(Ljava/lang/String;"
	       "Ljava/lang/String;)"
	       "Lorg/gnu/emacs/EmacsContextMenu;");
  FIND_METHOD (add_pane, "addPane", "(Ljava/lang/String;)V");
  FIND_METHOD (parent, "parent", "()Lorg/gnu/emacs/EmacsContextMenu;");
  FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;III)Z");
  FIND_METHOD (dismiss, "dismiss", "(Lorg/gnu/emacs/EmacsWindow;)V");

#undef FIND_METHOD
#undef FIND_METHOD_STATIC
}

static void
android_unwind_local_frame (void)
{
  (*android_java_env)->PopLocalFrame (android_java_env, NULL);
}

/* Push a local reference frame to the JVM stack and record it on the
   specpdl.  Release local references created within that frame when
   the specpdl is unwound past where it is after returning.  */

static void
android_push_local_frame (void)
{
  int rc;

  rc = (*android_java_env)->PushLocalFrame (android_java_env, 30);

  /* This means the JVM ran out of memory.  */
  if (rc < 1)
    android_exception_check ();

  record_unwind_protect_void (android_unwind_local_frame);
}

/* Data for android_dismiss_menu.  */

struct android_dismiss_menu_data
{
  /* The menu object.  */
  jobject menu;

  /* The window object.  */
  jobject window;
};

/* Cancel the context menu passed in POINTER.  Also, clear
   popup_activated_flag.  */

static void
android_dismiss_menu (void *pointer)
{
  struct android_dismiss_menu_data *data;

  data = pointer;
  (*android_java_env)->CallNonvirtualVoidMethod (android_java_env,
						 data->menu,
						 menu_class.class,
						 menu_class.dismiss,
						 data->window);
  popup_activated_flag = 0;
}

/* Recursively process events until a ANDROID_CONTEXT_MENU event
   arrives.  Then, return the item ID specified in the event in
   *ID.  */

static void
android_process_events_for_menu (int *id)
{
  int blocked;

  /* Set menu_event_id to -1; handle_one_android_event will set it to
     the event ID upon receiving a context menu event.  This can cause
     a non-local exit.  */
  x_display_list->menu_event_id = -1;

  /* Unblock input completely.  */
  blocked = interrupt_input_blocked;
  totally_unblock_input ();

  /* Now wait for the menu event ID to change.  */
  while (x_display_list->menu_event_id == -1)
    {
      /* Wait for events to become available.  */
      android_wait_event ();

      /* Process pending signals.  */
      process_pending_signals ();

      /* Maybe quit.  This is important because the framework (on
	 Android 4.0.3) can sometimes fail to deliver context menu
	 closed events if a submenu was opened, and the user still
	 needs to be able to quit.  */
      maybe_quit ();
    }

  /* Restore the input block.  */
  interrupt_input_blocked = blocked;

  /* Return the ID.  */
  *id = x_display_list->menu_event_id;
}

/* Structure describing a ``subprefix'' in the menu.  */

struct android_menu_subprefix
{
  /* The subprefix above.  */
  struct android_menu_subprefix *last;

  /* The subprefix itself.  */
  Lisp_Object subprefix;
};

/* Free the subprefixes starting from *DATA.  */

static void
android_free_subprefixes (void *data)
{
  struct android_menu_subprefix **head, *subprefix;

  head = data;

  while (*head)
    {
      subprefix = *head;
      *head = subprefix->last;

      xfree (subprefix);
    }
}

Lisp_Object
android_menu_show (struct frame *f, int x, int y, int menuflags,
		   Lisp_Object title, const char **error_name)
{
  jobject context_menu, current_context_menu;
  jobject title_string, help_string, temp;
  size_t i;
  Lisp_Object pane_name, prefix;
  specpdl_ref count, count1;
  Lisp_Object item_name, enable, def, tem, entry, type, selected;
  Lisp_Object help;
  jmethodID method;
  jobject store;
  bool rc;
  jobject window;
  int id, item_id, submenu_depth;
  struct android_dismiss_menu_data data;
  struct android_menu_subprefix *subprefix, *temp_subprefix;
  struct android_menu_subprefix *subprefix_1;
  bool checkmark;
  unsigned int serial;
  JNIEnv *env;

  count = SPECPDL_INDEX ();
  serial = ++current_menu_serial;
  env = android_java_env;

  block_input ();

  /* Push the first local frame.  */
  android_push_local_frame ();

  /* Set title_string to a Java string containing TITLE if non-nil.
     If the menu consists of more than one pane, replace the title
     with the pane header item so that the menu looks consistent.  */

  title_string = NULL;
  if (STRINGP (title) && menu_items_n_panes < 2)
    title_string = android_build_string (title, NULL);

  /* Push the first local frame for the context menu.  */
  method = menu_class.create_context_menu;
  current_context_menu = context_menu
    = (*android_java_env)->CallStaticObjectMethod (android_java_env,
						   menu_class.class,
						   method,
						   title_string);

  /* Delete the unused title reference.  */

  if (title_string)
    ANDROID_DELETE_LOCAL_REF (title_string);

  /* Push the second local frame for temporaries.  */
  count1 = SPECPDL_INDEX ();
  android_push_local_frame ();

  /* Iterate over the menu.  */
  i = 0, submenu_depth = 0;

  while (i < menu_items_used)
    {
      if (NILP (AREF (menu_items, i)))
	{
	  /* This is the start of a new submenu.  However, it can be
	     ignored here.  */
	  i += 1;
	  submenu_depth += 1;
	}
      else if (EQ (AREF (menu_items, i), Qlambda))
	{
	  /* This is the end of a submenu.  Go back to the previous
	     context menu.  */
	  store = current_context_menu;
	  current_context_menu
	    = (*env)->CallNonvirtualObjectMethod (env,
						  current_context_menu,
						  menu_class.class,
						  menu_class.parent);
	  android_exception_check ();

	  if (store != context_menu)
	    ANDROID_DELETE_LOCAL_REF (store);
	  i += 1;
	  submenu_depth -= 1;

	  if (!current_context_menu || submenu_depth < 0)
	    {
	      __android_log_print (ANDROID_LOG_FATAL, __func__,
				   "unbalanced submenu pop in menu_items");
	      emacs_abort ();
	    }
	}
      else if (EQ (AREF (menu_items, i), Qt)
	       && submenu_depth != 0)
	i += MENU_ITEMS_PANE_LENGTH;
      else if (EQ (AREF (menu_items, i), Qquote))
	i += 1;
      else if (EQ (AREF (menu_items, i), Qt))
	{
	  /* If the menu contains a single pane, then the pane is
	     actually TITLE.  Don't duplicate the text within the
	     context menu title.  */

	  if (menu_items_n_panes < 2)
	    goto next_item;

	  /* This is a new pane.  Switch back to the topmost context
	     menu.  */
	  if (current_context_menu != context_menu)
	    ANDROID_DELETE_LOCAL_REF (current_context_menu);
	  current_context_menu = context_menu;

	  /* Now figure out the title of this pane.  */
	  pane_name = AREF (menu_items, i + MENU_ITEMS_PANE_NAME);
	  prefix = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX);

	  /* PANE_NAME may be nil, in which case it must be set to an
	     empty string.  */

	  if (NILP (pane_name))
	    pane_name = empty_unibyte_string;

	  /* Remove the leading prefix character if need be.  */

	  if ((menuflags & MENU_KEYMAPS) && !NILP (prefix)
	      && SCHARS (prefix))
	    pane_name = Fsubstring (pane_name, make_fixnum (1), Qnil);

	  /* Add the pane.  */
	  temp = android_build_string (pane_name, NULL);
	  android_exception_check ();

	  (*env)->CallNonvirtualVoidMethod (env, current_context_menu,
					    menu_class.class,
					    menu_class.add_pane, temp);
	  android_exception_check ();
	  ANDROID_DELETE_LOCAL_REF (temp);

	next_item:
	  i += MENU_ITEMS_PANE_LENGTH;
	}
      else
	{
	  item_name = AREF (menu_items, i + MENU_ITEMS_ITEM_NAME);
	  enable = AREF (menu_items, i + MENU_ITEMS_ITEM_ENABLE);
	  def = AREF (menu_items, i + MENU_ITEMS_ITEM_DEFINITION);
	  type = AREF (menu_items, i + MENU_ITEMS_ITEM_TYPE);
	  selected = AREF (menu_items, i + MENU_ITEMS_ITEM_SELECTED);
	  help = AREF (menu_items, i + MENU_ITEMS_ITEM_HELP);

	  /* This is an actual menu item (or submenu).  Add it to the
	     menu.  */

	  if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used
	      && NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH)))
	    {
	      /* This is a submenu.  Add it.  */
	      title_string = (!NILP (item_name)
			      ? android_build_string (item_name, NULL)
			      : NULL);
	      help_string = NULL;

	      /* Menu items can have tool tips on Android 26 and
		 later.  In this case, set it to the help string.  */

	      if (android_get_current_api_level () >= 26
		  && STRINGP (help))
		help_string = android_build_string (help, NULL);

	      store = current_context_menu;
	      current_context_menu
		= (*env)->CallNonvirtualObjectMethod (env,
						      current_context_menu,
						      menu_class.class,
						      menu_class.add_submenu,
						      title_string,
						      help_string);
	      android_exception_check ();

	      if (store != context_menu)
		ANDROID_DELETE_LOCAL_REF (store);

	      if (title_string)
		ANDROID_DELETE_LOCAL_REF (title_string);

	      if (help_string)
		ANDROID_DELETE_LOCAL_REF (help_string);
	    }
	  else if (NILP (def) && menu_separator_name_p (SSDATA (item_name)))
	    /* Ignore this separator item.  */
	    ;
	  else
	    {
	      /* Compute the item ID.  This is the index of value.
		 Make sure it doesn't overflow.  */

	      if (ckd_add (&item_id, i + MENU_ITEMS_ITEM_VALUE, 0))
		memory_full (i + MENU_ITEMS_ITEM_VALUE * sizeof (Lisp_Object));

	      /* Add this menu item with the appropriate state.  */

	      title_string = (!NILP (item_name)
			      ? android_build_string (item_name, NULL)
			      : NULL);
	      help_string = NULL;

	      /* Menu items can have tool tips on Android 26 and
		 later.  In this case, set it to the help string.  */

	      if (android_get_current_api_level () >= 26
		  && STRINGP (help))
		help_string = android_build_string (help, NULL);

	      /* Determine whether or not to display a check box.  */

	      checkmark = (EQ (type, QCtoggle)
			   || EQ (type, QCradio));

	      (*env)->CallNonvirtualVoidMethod (env,
						current_context_menu,
						menu_class.class,
						menu_class.add_item,
						(jint) item_id,
						title_string,
						(jboolean) !NILP (enable),
						(jboolean) checkmark,
						(jboolean) !NILP (selected),
						help_string,
						(jboolean) (EQ (type,
								QCradio)));
	      android_exception_check ();

	      if (title_string)
		ANDROID_DELETE_LOCAL_REF (title_string);

	      if (help_string)
		ANDROID_DELETE_LOCAL_REF (help_string);
	    }

	  i += MENU_ITEMS_ITEM_LENGTH;
	}
    }

  /* The menu has now been built.  Pop the second local frame.  */
  unbind_to (count1, Qnil);

  /* Now, display the context menu.  */
  window = android_resolve_handle (FRAME_ANDROID_WINDOW (f));
  rc = (*env)->CallNonvirtualBooleanMethod (env, context_menu,
					    menu_class.class,
					    menu_class.display,
					    window, (jint) x,
					    (jint) y,
					    (jint) serial);
  android_exception_check ();

  if (!rc)
    /* This means displaying the menu failed.  */
    goto finish;

  /* Make sure the context menu is always dismissed.  */
  data.menu = context_menu;
  data.window = window;
  record_unwind_protect_ptr (android_dismiss_menu, &data);

  /* Next, process events waiting for something to be selected.  */
  popup_activated_flag = 1;
  android_process_events_for_menu (&id);

  if (!id)
    /* This means no menu item was selected.  */
    goto finish;

  /* This means the id is invalid.  */
  if (id >= ASIZE (menu_items))
    goto finish;

  /* Now return the menu item at that location.  */
  tem = Qnil;
  subprefix = NULL;
  record_unwind_protect_ptr (android_free_subprefixes, &subprefix);

  /* Find the selected item, and its pane, to return
     the proper value.  */

  prefix = entry = Qnil;
  i = 0;
  while (i < menu_items_used)
    {
      if (NILP (AREF (menu_items, i)))
	{
	  temp_subprefix = xmalloc (sizeof *temp_subprefix);
	  temp_subprefix->last = subprefix;
	  subprefix = temp_subprefix;
	  subprefix->subprefix = prefix;

	  prefix = entry;
	  i++;
	}
      else if (EQ (AREF (menu_items, i), Qlambda))
	{
	  prefix = subprefix->subprefix;
	  temp_subprefix = subprefix->last;
	  xfree (subprefix);
	  subprefix = temp_subprefix;

	  i++;
	}
      else if (EQ (AREF (menu_items, i), Qt))
	{
	  prefix
	    = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX);
	  i += MENU_ITEMS_PANE_LENGTH;
	}
      /* Ignore a nil in the item list.
	 It's meaningful only for dialog boxes.  */
      else if (EQ (AREF (menu_items, i), Qquote))
	i += 1;
      else
	{
	  entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE);

	  if (i + MENU_ITEMS_ITEM_VALUE == id)
	    {
	      if (menuflags & MENU_KEYMAPS)
		{
		  entry = list1 (entry);

		  if (!NILP (prefix))
		    entry = Fcons (prefix, entry);

		  for (subprefix_1 = subprefix; subprefix_1;
		       subprefix_1 = subprefix_1->last)
		    if (!NILP (subprefix_1->subprefix))
		      entry = Fcons (subprefix_1->subprefix, entry);
		}

	      tem = entry;
	    }
	  i += MENU_ITEMS_ITEM_LENGTH;
	}
    }

  unblock_input ();
  return unbind_to (count, tem);

 finish:
  unblock_input ();
  return unbind_to (count, Qnil);
}



/* Toolkit dialog implementation.  */

/* Structure describing the EmacsDialog class.  */

struct android_emacs_dialog
{
  jclass class;
  jmethodID create_dialog;
  jmethodID add_button;
  jmethodID display;
};

/* Identifiers associated with the EmacsDialog class.  */
static struct android_emacs_dialog dialog_class;

static void
android_init_emacs_dialog (void)
{
  jclass old;

  dialog_class.class
    = (*android_java_env)->FindClass (android_java_env,
				      "org/gnu/emacs/EmacsDialog");
  eassert (dialog_class.class);

  old = dialog_class.class;
  dialog_class.class
    = (jclass) (*android_java_env)->NewGlobalRef (android_java_env,
						  (jobject) old);
  ANDROID_DELETE_LOCAL_REF (old);

  if (!dialog_class.class)
    emacs_abort ();

#define FIND_METHOD(c_name, name, signature)			\
  dialog_class.c_name						\
    = (*android_java_env)->GetMethodID (android_java_env,	\
					dialog_class.class,	\
					name, signature);	\
  eassert (dialog_class.c_name);

#define FIND_METHOD_STATIC(c_name, name, signature)			\
  dialog_class.c_name							\
    = (*android_java_env)->GetStaticMethodID (android_java_env,		\
					      dialog_class.class,	\
					      name, signature);		\

  FIND_METHOD_STATIC (create_dialog, "createDialog", "(Ljava/lang/String;"
		      "Ljava/lang/String;I)Lorg/gnu/emacs/EmacsDialog;");
  FIND_METHOD (add_button, "addButton", "(Ljava/lang/String;IZ)V");
  FIND_METHOD (display, "display", "()Z");

#undef FIND_METHOD
#undef FIND_METHOD_STATIC
}

static Lisp_Object
android_dialog_show (struct frame *f, Lisp_Object title,
		     Lisp_Object header, const char **error_name)
{
  specpdl_ref count;
  jobject dialog, java_header, java_title, temp;
  size_t i;
  Lisp_Object item_name, enable, entry;
  bool rc;
  int id;
  jmethodID method;
  unsigned int serial;
  JNIEnv *env;

  /* Generate a unique ID for events from this dialog box.  */
  serial = ++current_menu_serial;

  if (menu_items_n_panes > 1)
    {
      *error_name = "Multiple panes in dialog box";
      return Qnil;
    }

  /* Do the initial setup.  */
  count = SPECPDL_INDEX ();
  *error_name = NULL;

  android_push_local_frame ();

  /* Figure out what header to use.  */
  java_header = (!NILP (header)
		 ? android_build_jstring ("Information")
		 : android_build_jstring ("Question"));

  /* And the title.  */
  java_title = android_build_string (title, NULL);

  /* Now create the dialog.  */
  method = dialog_class.create_dialog;
  dialog = (*android_java_env)->CallStaticObjectMethod (android_java_env,
							dialog_class.class,
							method, java_header,
						        java_title,
							(jint) serial);
  android_exception_check ();

  /* Delete now unused local references.  */
  if (java_header)
    ANDROID_DELETE_LOCAL_REF (java_header);
  ANDROID_DELETE_LOCAL_REF (java_title);

  /* Save the JNI environment pointer prior to constructing the
     dialog, as typing (*android_java_env)->... gives rise to very
     long lines.  */
  env = android_java_env;

  /* Create the buttons.  */
  i = MENU_ITEMS_PANE_LENGTH;
  while (i < menu_items_used)
    {
      item_name = AREF (menu_items, i + MENU_ITEMS_ITEM_NAME);
      enable = AREF (menu_items, i + MENU_ITEMS_ITEM_ENABLE);

      /* Verify that there is no submenu here.  */

      if (NILP (item_name))
	{
	  *error_name = "Submenu in dialog items";
	  return unbind_to (count, Qnil);
	}

      /* Skip past boundaries between buttons on different sides.  The
	 Android toolkit is too silly to understand this
	 distinction.  */

      if (EQ (item_name, Qquote))
	++i;
      else
	{
	  /* Make sure i is within bounds.  */
	  if (i > TYPE_MAXIMUM (jint))
	    {
	      *error_name = "Dialog box too big";
	      return unbind_to (count, Qnil);
	    }

	  /* Add the button.  */
	  temp = android_build_string (item_name, NULL);
	  (*env)->CallNonvirtualVoidMethod (env, dialog,
					    dialog_class.class,
					    dialog_class.add_button,
					    temp, (jint) i,
					    (jboolean) NILP (enable));
	  android_exception_check ();
	  ANDROID_DELETE_LOCAL_REF (temp);
	  i += MENU_ITEMS_ITEM_LENGTH;
	}
    }

  /* The dialog is now built.  Run it.  */
  rc = (*env)->CallNonvirtualBooleanMethod (env, dialog,
					    dialog_class.class,
					    dialog_class.display);
  android_exception_check ();

  if (!rc)
    quit ();

  /* Wait for the menu ID to arrive.  */
  android_process_events_for_menu (&id);

  if (!id)
    quit ();

  /* Find the selected item, and its pane, to return
     the proper value.  */
  i = 0;
  while (i < menu_items_used)
    {
      if (EQ (AREF (menu_items, i), Qt))
	i += MENU_ITEMS_PANE_LENGTH;
      else if (EQ (AREF (menu_items, i), Qquote))
	/* This is the boundary between left-side elts and right-side
	   elts.  */
	++i;
      else
	{
	  entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE);

	  if (id == i)
	    return entry;

	  i += MENU_ITEMS_ITEM_LENGTH;
	}
    }

  return Qnil;
}

Lisp_Object
android_popup_dialog (struct frame *f, Lisp_Object header,
		      Lisp_Object contents)
{
  Lisp_Object title;
  const char *error_name;
  Lisp_Object selection;
  specpdl_ref specpdl_count = SPECPDL_INDEX ();

  check_window_system (f);

  /* Decode the dialog items from what was specified.  */
  title = Fcar (contents);
  CHECK_STRING (title);
  record_unwind_protect_void (unuse_menu_items);

  if (NILP (Fcar (Fcdr (contents))))
    /* No buttons specified, add an "Ok" button so users can pop down
       the dialog.  */
    contents = list2 (title, Fcons (build_string ("Ok"), Qt));

  list_of_panes (list1 (contents));

  /* Display them in a dialog box.  */
  block_input ();
  selection = android_dialog_show (f, title, header, &error_name);
  unblock_input ();

  unbind_to (specpdl_count, Qnil);
  discard_menu_items ();

  if (error_name)
    error ("%s", error_name);

  return selection;
}

#else

int
popup_activated (void)
{
  return 0;
}

#endif

DEFUN ("menu-or-popup-active-p", Fmenu_or_popup_active_p,
       Smenu_or_popup_active_p, 0, 0, 0,
       doc: /* SKIP: real doc in xfns.c.  */)
  (void)
{
  return (popup_activated ()) ? Qt : Qnil;
}

void
init_androidmenu (void)
{
#ifndef ANDROID_STUBIFY
  android_init_emacs_context_menu ();
  android_init_emacs_dialog ();
#endif
}

void
syms_of_androidmenu (void)
{
  defsubr (&Smenu_or_popup_active_p);
}
