/*
 * SPDX-FileCopyrightText: 2011-2026 Sébastien Helleu <flashcode@flashtux.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * This file is part of WeeChat, the extensible chat client.
 *
 * WeeChat 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.
 *
 * WeeChat 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 WeeChat.  If not, see <https://www.gnu.org/licenses/>.
 */

/* Mouse functions for Curses GUI */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <math.h>

#include "../../core/weechat.h"
#include "../../core/core-config.h"
#include "../../core/core-hook.h"
#include "../../core/core-string.h"
#include "../../core/core-utf8.h"
#include "../../core/core-util.h"
#include "../../plugins/plugin.h"
#include "../gui-bar.h"
#include "../gui-bar-window.h"
#include "../gui-buffer.h"
#include "../gui-chat.h"
#include "../gui-completion.h"
#include "../gui-focus.h"
#include "../gui-input.h"
#include "../gui-key.h"
#include "../gui-mouse.h"
#include "../gui-window.h"
#include "gui-curses-mouse.h"


char *gui_mouse_wheel_utf8_codes[][2] =
{ { "`",  "wheelup"            },
  { "p",  "ctrl-wheelup"       },
  { "h",  "alt-wheelup"        },
  { "x",  "alt-ctrl-wheelup"   },
  { "a",  "wheeldown"          },
  { "q",  "ctrl-wheeldown"     },
  { "i",  "alt-wheeldown"      },
  { "y",  "alt-ctrl-wheeldown" },
  { NULL, NULL                 } };

char *gui_mouse_button_utf8_codes[][2] =
{ { " ",  "button1"            },
  { "\"", "button2"            },
  { "!",  "button3"            },
  { "b",  "button4"            },
  { "c",  "button5"            },
  { "d",  "button6"            },
  { "e",  "button7"            },
  { "f",  "button8"            },
  { "g",  "button9"            },
  { "0",  "ctrl-button1"       },
  { "2",  "ctrl-button2"       },
  { "1",  "ctrl-button3"       },
  { "(",  "alt-button1"        },
  { "*",  "alt-button2"        },
  { ")",  "alt-button3"        },
  { "8",  "alt-ctrl-button1"   },
  { ":",  "alt-ctrl-button2"   },
  { "9",  "alt-ctrl-button3"   },
  { NULL, NULL                 } };


/*
 * Enable mouse.
 */

void
gui_mouse_enable (void)
{
    gui_mouse_enabled = 1;
    fprintf (stderr, "\033[?1005h\033[?1006h\033[?1000h\033[?1002h");
    fflush (stderr);

    (void) hook_signal_send ("mouse_enabled",
                             WEECHAT_HOOK_SIGNAL_STRING, NULL);
}

/*
 * Disable mouse.
 */

void
gui_mouse_disable (void)
{
    gui_mouse_enabled = 0;
    fprintf (stderr, "\033[?1002l\033[?1000l\033[?1006l\033[?1005l");
    fflush (stderr);

    (void) hook_signal_send ("mouse_disabled",
                             WEECHAT_HOOK_SIGNAL_STRING, NULL);
}

/*
 * Display state of mouse.
 */

void
gui_mouse_display_state (void)
{
    gui_chat_printf (NULL,
                     gui_mouse_enabled ?
                     _("Mouse is enabled") : _("Mouse is disabled"));
}

/*
 * Initialize "grab mode".
 */

void
gui_mouse_grab_init (int area)
{
    gui_mouse_grab = (area) ? 2 : 1;
}

/*
 * Get area for input, according to (x,y) of mouse event.
 *
 * For example: @item(buffer_nicklist)
 *              @bar(title)
 *              @chat
 *              @*
 */

const char *
gui_mouse_grab_event2input (void)
{
    struct t_gui_focus_info *focus_info;
    static char area[256];

    area[0] = '\0';

    focus_info = gui_focus_get_info (gui_mouse_event_x[0],
                                     gui_mouse_event_y[0]);
    if (focus_info)
    {
        if (focus_info->bar_item)
        {
            snprintf (area, sizeof (area),
                      "@item(%s)", focus_info->bar_item);
        }
        else if (focus_info->bar_window)
        {
            snprintf (area, sizeof (area),
                      "@bar(%s)", ((focus_info->bar_window)->bar)->name);
        }
        else if (focus_info->chat)
        {
            snprintf (area, sizeof (area), "@chat");
        }
        else
        {
            snprintf (area, sizeof (area), "@*");
        }
        gui_focus_free_info (focus_info);
    }

    return area;
}

/*
 * End "grab mode".
 */

void
gui_mouse_grab_end (const char *mouse_key)
{
    char mouse_key_input[256];

    /* insert mouse key in input */
    if (gui_current_window->buffer->input)
    {
        if (gui_mouse_grab == 2)
        {
            /* mouse key with area */
            snprintf (mouse_key_input, sizeof (mouse_key_input),
                      "%s:%s",
                      gui_mouse_grab_event2input (),
                      mouse_key);
        }
        else
        {
            /* mouse key without area */
            snprintf (mouse_key_input, sizeof (mouse_key_input),
                      "%s", mouse_key);
        }
        gui_input_insert_string (gui_current_window->buffer, mouse_key_input);
        gui_input_text_changed_modifier_and_signal (gui_current_window->buffer,
                                                    1, /* save undo */
                                                    1); /* stop completion */
    }

    gui_mouse_grab = 0;
}

/*
 * Return size of mouse event (SGR and UTF-8 events are supported):
 *     -1: not a mouse event
 *      0: incomplete mouse event
 *    > 0: complete mouse event
 */

int
gui_mouse_event_size (const char *key)
{
    const char *ptr_key;
    int i, key_valid_utf8;

    if (!key || !key[0])
        return -1;

    if (strncmp (key, "\x01[[<", 4) == 0)
    {
        /*
         * SGR event: digits separated by ';', ending by 'M' (pressed)
         * or 'm' (released), example: "0;71;21M"
         */
        for (ptr_key = key + 4; ptr_key[0]; ptr_key++)
        {
            if ((ptr_key[0] != ';') && !isdigit ((unsigned char)ptr_key[0]))
                return ptr_key - key + 1;
        }
        return 0;
    }
    else if (strncmp (key, "\x01[[M", 4) == 0)
    {
        /* UTF-8 event: 3 UTF-8 chars, example: "!4&" */
        ptr_key = key + 4;
        key_valid_utf8 = utf8_is_valid (ptr_key, -1, NULL);
        for (i = 0; i < 3; i++)
        {
            if (!ptr_key[0])
                return 0;
            ptr_key = (key_valid_utf8) ? utf8_next_char (ptr_key) : ptr_key + 1;
            if (!ptr_key)
                return 0;
        }
        return ptr_key - key;
    }

    /* invalid mouse event, or not supported */
    return -1;
}

/*
 * Concatenate the mouse event gesture in a key (containing the button name).
 *
 * Note: *key must be long enough for the gesture added by this function
 * (see below).
 *
 * Mouse gesture: if (x,y) on release is different from (x,y) on click,
 * compute distance and angle between 2 points.
 *
 * Distance: sqrt((x2-x1)²+(y2-y1)²)
 * Angle   : atan2(x1-x1, y2-y1)
 *
 * Angle:
 *
 *              3.14             pi
 *               /\
 *       -2.35   ||   2.35       3/4 * pi
 *               ||
 *   -1.57  /----++----\  1.57   1/2 * pi
 *          \----++----/
 *               ||
 *       -0.78   ||   0.78       1/4 * pi
 *               \/
 *              0.00             0
 *
 * Possible returned gestures are:
 *
 *   value returned        | dist. | angle
 *   ----------------------+-------+--------------------------
 *   "-gesture-up"         | 3..19 | -2.35..-3.14 + 2.35..3.14
 *   "-gesture-up-long"    | >= 20 |
 *   "-gesture-down"       | 3..19 | -0.78..0.78
 *   "-gesture-down-long"  | >= 20 |
 *   "-gesture-left"       | 3..39 | -0.78..-2.35
 *   "-gesture-left-long"  | >= 40 |
 *   "-gesture-right"      | 3..39 |  0.78..2.35
 *   "-gesture-right-long" | >= 40 |
 */

void
gui_mouse_event_concat_gesture (char *key)
{
    double diff_x, diff_y, distance, angle, pi4;

    /* (x, y) == (x2, y2) => no gesture, do not add anything */
    if ((gui_mouse_event_x[0] == gui_mouse_event_x[1])
        && (gui_mouse_event_y[0] == gui_mouse_event_y[1]))
        return;

    diff_x = gui_mouse_event_x[1] - gui_mouse_event_x[0];
    diff_y = gui_mouse_event_y[1] - gui_mouse_event_y[0];
    distance = sqrt ((diff_x * diff_x) + (diff_y * diff_y));
    if (distance >= 3)
    {
        angle = atan2 ((double)(gui_mouse_event_x[1] - gui_mouse_event_x[0]),
                       (double)(gui_mouse_event_y[1] - gui_mouse_event_y[0]));
        pi4 = 3.14159265358979 / 4;
        if ((angle <= pi4 * (-3)) || (angle >= pi4 * 3))
        {
            strcat (key, "-gesture-up");
            if (distance >= 20)
                strcat (key, "-long");
        }
        else if ((angle >= pi4 * (-1)) && (angle <= pi4))
        {
            strcat (key, "-gesture-down");
            if (distance >= 20)
                strcat (key, "-long");
        }
        else if ((angle >= pi4 * (-3)) && (angle <= pi4 * (-1)))
        {
            strcat (key, "-gesture-left");
            if (distance >= 40)
                strcat (key, "-long");
        }
        else if ((angle >= pi4) && (angle <= pi4 * 3))
        {
            strcat (key, "-gesture-right");
            if (distance >= 40)
                strcat (key, "-long");
        }
    }
}

/*
 * Get mouse event name with a SGR mouse event.
 */

const char *
gui_mouse_event_name_sgr (const char *key)
{
    int length, num_items, is_release, button, x, y;
    char **items;
    static char mouse_key[128];

    if (!key || !key[0])
        return NULL;

    mouse_key[0] = '\0';

    items = string_split (key, ";", NULL, 0, 0, &num_items);
    if (!items)
        return NULL;

    if (num_items < 3)
        goto error;

    if (!util_parse_int (items[0], 10, &button))
        goto error;

    if (!util_parse_int (items[1], 10, &x))
        goto error;
    x = (x >= 1) ? x - 1 : 0;

    length = (int)strlen (items[2]);
    if (length <= 0)
        goto error;
    is_release = (items[2][length - 1] == 'm') ? 1 : 0;
    items[2][length - 1] = '\0';
    if (!util_parse_int (items[2], 10, &y))
        goto error;
    y = (y >= 1) ? y - 1 : 0;

    /* set data in "gui_mouse_event_xxx" */
    gui_mouse_event_x[gui_mouse_event_index] = x;
    gui_mouse_event_y[gui_mouse_event_index] = y;

    /* keep same coordinates if it's release code received as first event */
    if ((gui_mouse_event_index == 0) && is_release)
    {
        gui_mouse_event_index = 1;
        gui_mouse_event_x[1] = gui_mouse_event_x[0];
        gui_mouse_event_y[1] = gui_mouse_event_y[0];
    }

    if (gui_mouse_event_index == 0)
        gui_mouse_event_index = 1;

    if (button & 8)
        strcat (mouse_key, "alt-");
    if (button & 16)
        strcat (mouse_key, "ctrl-");
    if (button & 4)
        strcat (mouse_key, "shift-");

    if (button & 64)
    {
        if ((button & 3) == 0)
            strcat (mouse_key, "wheelup");
        else if ((button & 3) == 1)
            strcat (mouse_key, "wheeldown");
        /* not yet supported: wheel left/right */
        /*
        else if ((button & 3) == 2)
            strcat (mouse_key, "wheelleft");
        else if ((button & 3) == 3)
            strcat (mouse_key, "wheelright");
        */
        gui_mouse_event_x[1] = gui_mouse_event_x[0];
        gui_mouse_event_y[1] = gui_mouse_event_y[0];
        goto end;
    }
    else if (button & 128)
    {
        if ((button & 3) == 0)
            strcat (mouse_key, "button8");
        else if ((button & 3) == 1)
            strcat (mouse_key, "button9");
        else if ((button & 3) == 2)
            strcat (mouse_key, "button10");
        else if ((button & 3) == 3)
            strcat (mouse_key, "button11");
    }
    else
    {
        if ((button & 3) == 0)
            strcat (mouse_key, "button1");
        else if ((button & 3) == 1)
            strcat (mouse_key, "button3");
        else if ((button & 3) == 2)
            strcat (mouse_key, "button2");
    }

    if (!is_release && !(button & 64))
    {
        strcat (mouse_key, "-event-");
        if (button & 32)
        {
            strcat (mouse_key, "drag");
        }
        else
        {
            gui_mouse_event_x[1] = gui_mouse_event_x[0];
            gui_mouse_event_y[1] = gui_mouse_event_y[0];
            strcat (mouse_key, "down");
        }
        goto end;
    }

    gui_mouse_event_concat_gesture (mouse_key);

end:
    string_free_split (items);
    return mouse_key;

error:
    string_free_split (items);
    return NULL;
}

/*
 * Get mouse event name with a UTF-8 mouse event: if the key is invalid UTF-8,
 * use the 3 bytes, otherwise 3 UTF-8 chars.
 */

const char *
gui_mouse_event_name_utf8 (const char *key)
{
    int i, x, y, key_valid_utf8, length;
    static char mouse_key[128];
    const char *ptr_key;

    if (!key || !key[0])
        return NULL;

    mouse_key[0] = '\0';

    /*
     * key must have at least:
     *   one code (for event) + X + Y == 3 bytes or 3 UTF-8 chars
     */
    key_valid_utf8 = utf8_is_valid (key, -1, NULL);
    length = (key_valid_utf8) ? utf8_strlen (key) : (int)strlen (key);
    if (length < 3)
        return NULL;

    /* get coordinates and button */
    if (key_valid_utf8)
    {
        /* get coordinates using UTF-8 chars in key */
        x = utf8_char_int (key + 1) - 33;
        ptr_key = (char *)utf8_next_char (key + 1);
        if (!ptr_key)
            return NULL;
        y = utf8_char_int (ptr_key) - 33;
    }
    else
    {
        /* get coordinates using ISO chars in key */
        x = ((unsigned char)key[1]) - 33;
        y = ((unsigned char)key[2]) - 33;
    }
    if (x < 0)
        x = 0;
    if (y < 0)
        y = 0;

    /* ignore key if it's motion/end code received as first event */
    if ((gui_mouse_event_index == 0)
        && (MOUSE_CODE_UTF8_MOTION(key[0]) || MOUSE_CODE_UTF8_END(key[0])))
    {
        return NULL;
    }

    /* set data in "gui_mouse_event_xxx" */
    gui_mouse_event_x[gui_mouse_event_index] = x;
    gui_mouse_event_y[gui_mouse_event_index] = y;
    if (gui_mouse_event_index == 0)
        gui_mouse_event_button = key[0];

    if (gui_mouse_event_index == 0)
        gui_mouse_event_index = 1;

    /*
     * browse wheel codes, if one code is found, return event name immediately
     */
    for (i = 0; gui_mouse_wheel_utf8_codes[i][0]; i++)
    {
        if (key[0] == gui_mouse_wheel_utf8_codes[i][0][0])
        {
            strcat (mouse_key, gui_mouse_wheel_utf8_codes[i][1]);
            gui_mouse_event_x[1] = gui_mouse_event_x[0];
            gui_mouse_event_y[1] = gui_mouse_event_y[0];
            return mouse_key;
        }
    }

    /* add name of button event */
    for (i = 0; gui_mouse_button_utf8_codes[i][0]; i++)
    {
        if (gui_mouse_event_button == gui_mouse_button_utf8_codes[i][0][0])
        {
            strcat (mouse_key, gui_mouse_button_utf8_codes[i][1]);
            break;
        }
    }

    /* nothing found, reset now or mouse will be stuck */
    if (!mouse_key[0])
    {
        gui_mouse_event_reset ();
        return NULL;
    }

    if (!MOUSE_CODE_UTF8_END(key[0]))
    {
        strcat (mouse_key, "-event-");
        if (MOUSE_CODE_UTF8_MOTION(key[0])) {
            strcat (mouse_key, "drag");
        }
        else
        {
            gui_mouse_event_x[1] = gui_mouse_event_x[0];
            gui_mouse_event_y[1] = gui_mouse_event_y[0];
            strcat (mouse_key, "down");
        }
        return mouse_key;
    }

    gui_mouse_event_concat_gesture (mouse_key);

    return mouse_key;
}

/*
 * Process a mouse event.
 */

void
gui_mouse_event_process (const char *key)
{
    const char *mouse_key;
    int bare_event;

    /* get mouse event name */
    mouse_key = NULL;
    if (strncmp (key, "\x01[[<", 4) == 0)
        mouse_key = gui_mouse_event_name_sgr (key + 4);
    else if (strncmp (key, "\x01[[M", 4) == 0)
        mouse_key = gui_mouse_event_name_utf8 (key + 4);

    if (mouse_key && mouse_key[0])
    {
        bare_event = string_match (mouse_key, "*-event-*", 1);
        if (gui_mouse_grab)
        {
            if (!bare_event)
                gui_mouse_grab_end (mouse_key);
        }
        else if (!gui_key_debug)
        {
            /* execute command (if found) */
            (void) gui_key_focus (mouse_key, GUI_KEY_CONTEXT_MOUSE);
        }
        if (!bare_event)
            gui_mouse_event_reset ();
    }
}
