/* scrot_selection.c

Copyright 2020-2022 Daniel T. Borelli <danieltborelli@gmail.com>
Copyright 2021-2023 Guilherme Janczak <guilherme.janczak@yandex.com>
Copyright 2021      Martin C <martincation@protonmail.com>
Copyright 2021      Peter Wu <peterwu@hotmail.com>
Copyright 2021      Wilson Smith <01wsmith+gh@gmail.com>
Copyright 2022-2026 NRK <nrk@disroot.org>
Copyright 2024      7xnl <7xnl@proton.me>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies of the Software and its documentation and acknowledgment shall be
given in the documentation and software packages that this Software was
used.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

*/

/*
    This file is part of the scrot project.
    Part of the code comes from the scrot.c file and maintains its authorship.
*/

#include <err.h>
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <Imlib2.h>
#include <X11/Xlib.h>
#include <X11/cursorfont.h>
#include <X11/keysym.h>

#include "options.h"
#include "scrot.h"
#include "scrot_selection.h"
#include "selection_classic.h"
#include "selection_edge.h"
#include "util.h"

static void scrotSelectionCreate(void);
static void scrotSelectionDestroy(void);
static void scrotSelectionMotionDraw(int, int, int, int);
static bool scrotSelectionGetUserSel(struct SelectionRect *);

struct Selection selection;

static void createCursors(void)
{
    struct Selection *const sel = &selection;

    if (opt.selection.mode == SELECTION_MODE_CAPTURE)
        sel->curCross = XCreateFontCursor(disp, XC_cross);
    else if (opt.selection.mode == SELECTION_MODE_HIDE)
        sel->curCross = XCreateFontCursor(disp, XC_spraycan);
    else if (opt.selection.mode == SELECTION_MODE_BLUR)
        sel->curCross = XCreateFontCursor(disp, XC_box_spiral);
    else // SELECTION_MODE_HOLE
        sel->curCross = XCreateFontCursor(disp, XC_target);
    sel->curAngleNE = XCreateFontCursor(disp, XC_ur_angle);
    sel->curAngleNW = XCreateFontCursor(disp, XC_ul_angle);
    sel->curAngleSE = XCreateFontCursor(disp, XC_lr_angle);
    sel->curAngleSW = XCreateFontCursor(disp, XC_ll_angle);
}

static void freeCursors(void)
{
    struct Selection *const sel = &selection;

    XFreeCursor(disp, sel->curCross);
    XFreeCursor(disp, sel->curAngleNE);
    XFreeCursor(disp, sel->curAngleNW);
    XFreeCursor(disp, sel->curAngleSE);
    XFreeCursor(disp, sel->curAngleSW);
}

void selectionCalculateRect(int x0, int y0, int x1, int y1)
{
    struct SelectionRect *const rect = &selection.rect;

    rect->x = x0;
    rect->y = y0;
    rect->w = x1 - x0;
    rect->h = y1 - y0;

    if (rect->w == 0)
        rect->w++;

    if (rect->h == 0)
        rect->h++;

    if (rect->w < 0) {
        rect->x += rect->w;
        rect->w = 0 - rect->w;
    }

    if (rect->h < 0) {
        rect->y += rect->h;
        rect->h = 0 - rect->h;
    }
}

static void scrotSelectionCreate(void)
{
    struct Selection *const sel = &selection;
    *sel = (struct Selection){0};

    createCursors();

    if (opt.lineMode == LINE_MODE_CLASSIC) {
        sel->create = selectionClassicCreate;
        sel->draw = selectionClassicDraw;
        sel->motionDraw = selectionClassicMotionDraw;
        sel->destroy = selectionClassicDestroy;
    } else if (opt.lineMode == LINE_MODE_EDGE) {
        sel->create = selectionEdgeCreate;
        sel->draw = selectionEdgeDraw;
        sel->motionDraw = selectionEdgeMotionDraw;
        sel->destroy = selectionEdgeDestroy;
    } else {
        // It never happened, fix the options.c file
        scrotAssert(0);
    }

    sel->create();

    unsigned int const EVENT_MASK = ButtonMotionMask | ButtonPressMask | ButtonReleaseMask;

    int ret;
    struct timespec t = clockNow();
    for (int attempts = 0; attempts < 20; ++attempts) {
        ret = XGrabPointer(disp, root, False, EVENT_MASK, GrabModeAsync,
            GrabModeAsync, root, sel->curCross, CurrentTime);
        if (ret != AlreadyGrabbed)
            break;
        t = scrotSleepFor(t, 50);
    }
    if (ret != GrabSuccess) {
        scrotSelectionDestroy();
        errx(EXIT_FAILURE, "couldn't grab pointer");
    }
}

static void scrotSelectionDestroy(void)
{
    XUngrabPointer(disp, CurrentTime);
    freeCursors();
    selection.destroy();
    XSync(disp, False);
    /* HACK: although we destroyed the selection, the frame still might not
     * have been updated. a compositor might also buffer frames adding
     * latency. so wait a bit for the screen to update and the selection
     * borders to go away. */
    scrotSleepFor(clockNow(), 80);
}

static void scrotSelectionMotionDraw(int x0, int y0, int x1, int y1)
{
    const struct Selection *const sel = &selection;
    const unsigned int EVENT_MASK = ButtonMotionMask | ButtonPressMask | ButtonReleaseMask;
    Cursor cursor = None;

    if (x1 > x0 && y1 > y0)
        cursor = sel->curAngleSE;
    else if (x1 > x0)
        cursor = sel->curAngleNE;
    else if (y1 > y0)
        cursor = sel->curAngleSW;
    else
        cursor = sel->curAngleNW;
    XChangeActivePointerGrab(disp, EVENT_MASK, cursor, CurrentTime);
    sel->motionDraw(x0, y0, x1, y1);
}

XColor scrotSelectionGetLineColor(void)
{
    scrotAssert(opt.lineColor != NULL);

    XColor color;
    Colormap cmap = XDefaultColormap(disp, DefaultScreen(disp));
    if (!XAllocNamedColor(disp, cmap, opt.lineColor, &color, &(XColor){0})) {
        scrotSelectionDestroy();
        errx(EXIT_FAILURE, "Error allocating color: %s", opt.lineColor);
    }
    return color;
}

static bool scrotSelectionGetUserSel(struct SelectionRect *selectionRect)
{
    XEvent ev;
    enum { WAIT, DONE, ABORT } done = WAIT;
    int rx = 0, ry = 0, rw = 0, rh = 0;
    bool isButtonPressed = false;
    unsigned int buttonId = -1;
    Window target = None;
    Status ret;

    scrotSelectionCreate();

    struct timespec t = clockNow();
    for (int attempts = 0; attempts < 20; ++attempts) {
        ret = XGrabKeyboard(disp, root, False, GrabModeAsync, GrabModeAsync, CurrentTime);
        if (ret != AlreadyGrabbed)
            break;
        t = scrotSleepFor(t, 50);
    }
    if (ret != GrabSuccess) {
        scrotSelectionDestroy();
        errx(EXIT_FAILURE, "failed to grab keyboard");
    }

    while (done == WAIT) {
        XNextEvent(disp, &ev);
        switch (ev.type) {
        case MotionNotify:
            if (isButtonPressed)
                scrotSelectionMotionDraw(rx, ry, ev.xmotion.x, ev.xmotion.y);
            break;
        case ButtonPress:
            if (!isButtonPressed) {
                isButtonPressed = true;
                buttonId = ev.xbutton.button;
                rx = ev.xbutton.x;
                ry = ev.xbutton.y;
                target = scrotGetWindow(disp, ev.xbutton.subwindow, ev.xbutton.x, ev.xbutton.y);
                if (target == None)
                    target = root;
            } else {
                warnx("Alternate button pressed, aborting shot");
                done = ABORT;
            }
            break;
        case ButtonRelease:
            if (isButtonPressed && ev.xbutton.button == buttonId)
                done = DONE;
            break;
        case KeyPress:
        {
            KeySym *keysym = NULL;
            int keycode; /*dummy*/

            keysym = XGetKeyboardMapping(disp, ev.xkey.keycode, 1, &keycode);

            if (!keysym)
                break;

            if (!isButtonPressed) {
            key_abort_shot:
                if (!opt.ignoreKeyboard || *keysym == XK_Escape) {
                    warnx("Key was pressed, aborting shot");
                    done = ABORT;
                }
                XFree(keysym);
                break;
            }

            switch (*keysym) {
            case XK_Right:
                if (++rx > scr->width)
                    rx = scr->width;
                break;
            case XK_Left:
                if (--rx < 0)
                    rx = 0;
                break;
            case XK_Down:
                if (++ry > scr->height)
                    ry = scr->height;
                break;
            case XK_Up:
                if (--ry < 0)
                    ry = 0;
                break;
            default:
                goto key_abort_shot;
            }
            XFree(keysym);
            scrotSelectionMotionDraw(rx, ry, ev.xkey.x, ev.xkey.y);
            break;
        }
        case DestroyNotify:
            errx(EXIT_FAILURE, "received DestroyNotify event");
            break;
        default:
            /* ignore */
            break;
        }
    }
    selection.draw();

    XUngrabKeyboard(disp, CurrentTime);

    const bool isAreaSelect = (selection.rect.w > 5);

    scrotSelectionDestroy();

    if (done == ABORT)
        return false;

    if (isAreaSelect) {
        /* If a rect has been drawn, it's an area selection */
        rw = ev.xbutton.x - rx;
        rh = ev.xbutton.y - ry;

        if ((ev.xbutton.x + 1) == WidthOfScreen(scr))
            ++rw;

        if ((ev.xbutton.y + 1) == HeightOfScreen(scr))
            ++rh;

        if (rw < 0) {
            rx += rw;
            rw = 0 - rw;
        }

        if (rh < 0) {
            ry += rh;
            rh = 0 - rh;
        }

        // Not record pointer if there is a selection area because it is busy on that,
        // unless the delay option is used.
        if (opt.delay == 0)
            opt.pointer = false;
    } else {
        /* else it's a window click */
        if (!scrotGetGeometry(target, &rx, &ry, &rw, &rh))
            return false;
        clientWindow = target;
    }
    scrotNiceClip(&rx, &ry, &rw, &rh);

    selectionRect->x = rx;
    selectionRect->y = ry;
    selectionRect->w = rw;
    selectionRect->h = rh;
    return true;
}

static void changeImageOpacity(Imlib_Image image, const int opacity)
{
    imlib_context_set_image(image);
    const int w = imlib_image_get_width();
    const int h = imlib_image_get_height();

    uint32_t *data = imlib_image_get_data();
    uint32_t *end = data + (h * w);

    for (uint32_t *pixel = data; pixel != end; ++pixel) {
        const uint32_t a = (*pixel >> 24) * opacity / 255;
       *pixel = (a << 24) | (*pixel & 0x00FFFFFF);
    }

    imlib_image_put_back_data(data);
}

static Imlib_Image loadImage(const char *const fileName, const int opacity)
{
    Imlib_Image image = imlib_load_image(fileName);

    if (!image) {
        errx(EXIT_FAILURE, "option --select: Failed to load image:%s",
            fileName);
    }

    imlib_context_set_image(image);

    if (imlib_image_has_alpha() == 0) {
        warnx("Warning, ignoring the opacity parameter because the image '%s'"
            " has no alpha channel, it will be drawn fully opaque.", fileName);
        return image;
    }

    if (opacity == 255) {
        // Do nothing if a totally opaque image is expected.
        return image;
    }

    changeImageOpacity(image, opacity);

    return image;
}

Imlib_Image scrotSelectionSelectMode(void)
{
    struct SelectionRect rect0, rect1;
    Imlib_Image capture = NULL;

    const unsigned int oldMode = opt.selection.mode;
    opt.selection.mode = SELECTION_MODE_CAPTURE;

    if (opt.lineMode == LINE_MODE_AUTO) {
        /* edge mode doesn't work well in combination with --freeze. */
        opt.lineMode = opt.freeze ? LINE_MODE_CLASSIC : LINE_MODE_EDGE;
    }

    if (opt.lineWidth == 0) {
        int width = (scr->height + scr->width) / (scr->mheight + scr->mwidth);
        opt.lineWidth = MIN(MAX(width/4, 1), 8);
    }

    if (opt.delaySelection)
        scrotDoDelay();

    if (opt.freeze) {
        XGrabServer(disp);
        // capture immidately to avoid the selection making a mess later
        capture = imlib_create_image_from_drawable(0, 0, 0,
            scr->width, scr->height, false);
        if (!capture)
            errx(EXIT_FAILURE, "Failed to grab image");
    }

    bool selected = scrotSelectionGetUserSel(&rect0);
    if (selected) {
        opt.selection.mode = oldMode;
        if (opt.selection.mode & SELECTION_MODE_NOT_CAPTURE)
            selected = scrotSelectionGetUserSel(&rect1);
    }

    if (selected && !opt.delaySelection) {
        // this doesn't seem to make much sense if `--freeze` is enabled...
        opt.delayStart = clockNow();
        scrotDoDelay();
    }

    if (opt.freeze) {
        XUngrabServer(disp);
        XFlush(disp);
        // captured already, just crop it
        imlib_context_set_image(capture);
        if (selected) {
            capture = imlib_create_cropped_image(
                rect0.x, rect0.y, rect0.w, rect0.h);
        } else {
            capture = NULL;
        }
        imlib_free_image(); // frees the uncropped capture that's in context
        if (capture && opt.pointer)
            scrotGrabMousePointer(capture, rect0.x, rect0.y);
    } else if (selected) {
        capture = scrotGrabRectAndPointer(rect0.x, rect0.y, rect0.w, rect0.h);
    }

    if (capture == NULL) {
        if (selected)
            errx(EXIT_FAILURE, "Failed to grab image");
        return NULL;
    }

    if (opt.selection.mode == SELECTION_MODE_CAPTURE)
        return capture;

    XColor color = scrotSelectionGetLineColor();

    const int x = rect1.x - rect0.x; /* NOLINT(*UndefinedBinaryOperatorResult) */
    const int y = rect1.y - rect0.y;
    const int opacity = opt.lineOpacity;

    imlib_context_set_image(capture);

    switch (opt.selection.mode) {
    case SELECTION_MODE_HOLE:
        if (opacity > 0) {
            Imlib_Image hole = imlib_clone_image();
            imlib_context_set_color(color.red, color.green, color.blue, opacity);
            imlib_image_fill_rectangle(0, 0, rect0.w, rect0.h);
            imlib_blend_image_onto_image(hole, 0, x, y, rect1.w, rect1.h, x, y, rect1.w, rect1.h);
            imlib_context_set_image(hole);
            imlib_free_image_and_decache();
        }
        break;
    case SELECTION_MODE_HIDE:
    {
        const char *const fileName = opt.selection.fileName;

        if (fileName) {
            if (opacity > 0) {
                Imlib_Image hide = loadImage(fileName, opacity);
                imlib_context_set_image(hide);
                int const w = imlib_image_get_width();
                int const h = imlib_image_get_height();
                imlib_context_set_image(capture);
                imlib_blend_image_onto_image(hide, 0, 0, 0, w, h, x, y, rect1.w, rect1.h);
                imlib_context_set_image(hide);
                imlib_free_image_and_decache();
            }
        } else {
            imlib_context_set_color(color.red, color.green, color.blue, opacity);
            imlib_image_fill_rectangle(x, y, rect1.w, rect1.h);
        }
        break;
    }
    case SELECTION_MODE_BLUR:
    {
        const int amountBlur = opt.selection.blur;
        Imlib_Image blur = imlib_clone_image();
        imlib_context_set_image(blur);
        imlib_image_blur(amountBlur);
        imlib_context_set_image(capture);
        imlib_blend_image_onto_image(blur, 0, x, y, rect1.w, rect1.h, x, y, rect1.w, rect1.h);
        imlib_context_set_image(blur);
        imlib_free_image_and_decache();
        break;
    }
    default:
        scrotAssert(0 && "unreachable");
    }

    return capture;
}
