/*
 * Copyright (C) 2000-2007 Carsten Haitzler, Geoff Harrison and various contributors
 * Copyright (C) 2004-2021 Kim Woelders
 *
 * 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, its documentation and marketing & publicity
 * materials, and acknowledgment shall be given in the documentation, materials
 * 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.
 */
#include "config.h"

#include <X11/Xlib.h>

#include "E.h"
#include "aclass.h"
#include "borders.h"
#include "buttons.h"
#include "cursors.h"
#include "desktops.h"
#include "emodule.h"
#include "eobj.h"
#include "ewins.h"
#include "grabs.h"
#include "list.h"
#include "slide.h"
#include "xwin.h"

#define SLIDEOUT_EVENT_MASK \
  (KeyPressMask | KeyReleaseMask | \
   ButtonPressMask | ButtonReleaseMask | EnterWindowMask | LeaveWindowMask | \
   PointerMotionMask)

typedef struct {
    dlist_t         list;
    EObj            o;
    char           *name;
    char            direction;
    int             num_objs;
    EObj          **objs;
    unsigned int    ref_count;
    EWin           *context_ewin;
} Slideout;

#define DIR_LEFT        0
#define DIR_RIGHT       1
#define DIR_UP          2
#define DIR_DOWN        3

static          LIST_HEAD(slideout_list);

static struct {
    Slideout       *active;
} Mode_slideouts = {
    NULL
};

static void     SlideoutHandleEvent(Win win, XEvent * ev, void *prm);

static Slideout *
SlideoutCreate(const char *name, char dir)
{
    Slideout       *s;

    s = ECALLOC(Slideout, 1);
    if (!s)
        return NULL;

    s->name = Estrdup(name);
    s->direction = dir;

    EoInit(s, EOBJ_TYPE_MISC, NoXID, -10, -10, 1, 1, 1, name);
    EoSetShadow(s, 1);
    ESelectInput(EoGetWin(s), SLIDEOUT_EVENT_MASK);
    EventCallbackRegister(EoGetWin(s), SlideoutHandleEvent, s);

    return s;
}

static void
SlideoutCalcSize(Slideout *s)
{
    int             i;
    int             sw, sh, bw, bh;

    sw = sh = 0;
    for (i = 0; i < s->num_objs; i++)
    {
        bw = EobjGetW(s->objs[i]);
        bh = EobjGetH(s->objs[i]);

        switch (s->direction)
        {
        case DIR_UP:
        case DIR_DOWN:
            if (bw > sw)
                sw = bw;
            sh += bh;
            break;
        case DIR_LEFT:
        case DIR_RIGHT:
            if (bh > sh)
                sh = bh;
            sw += bw;
            break;
        default:
            break;
        }
    }

    EoResize(s, sw, sh);
}

static void
SlideoutArrange(Slideout *s, int dir)
{
    int             i, x, y;
    int             sw, sh, bw, bh;

    x = y = 0;
    sw = EoGetW(s);
    sh = EoGetH(s);

    for (i = 0; i < s->num_objs; i++)
    {
        bw = EobjGetW(s->objs[i]);
        bh = EobjGetH(s->objs[i]);

        switch (dir)
        {
        case DIR_UP:
            y += bh;
            EMoveWindow(EobjGetWin(s->objs[i]), (sw - bw) >> 1, sh - y);
            break;
        case DIR_DOWN:
            EMoveWindow(EobjGetWin(s->objs[i]), (sw - bw) >> 1, y);
            y += bh;
            break;
        case DIR_LEFT:
            x += bw;
            EMoveWindow(EobjGetWin(s->objs[i]), sw - x, (sh - bh) >> 1);
            break;
        case DIR_RIGHT:
            EMoveWindow(EobjGetWin(s->objs[i]), x, (sh - bh) >> 1);
            x += bw;
            break;
        default:
            break;
        }
    }
    EShapePropagate(EoGetWin(s));
}

static void
SlideoutShow(Slideout *s, EWin *ewin, Win win)
{
    int             x, y, i, xx, yy, sw, sh;
    int             dir;
    XSetWindowAttributes att;
    int             w, h;
    Desk           *dsk;

    /* Don't ever show more than one slideout */
    if (Mode_slideouts.active)
        return;

    SlideoutCalcSize(s);
    ETranslateCoordinates(win, VROOT, 0, 0, &x, &y, NULL);

    w = WinGetW(win);
    h = WinGetH(win);
    sw = EoGetW(s);
    sh = EoGetH(s);
    xx = 0;
    yy = 0;

    dir = s->direction;
    switch (dir)
    {
    case DIR_UP:
        xx = x + ((w - sw) >> 1);
        yy = y - sh;
        if (yy < 0 && WinGetH(VROOT) - (y + h) > y)
        {
            dir = DIR_DOWN;
            yy = y + h;
        }
        break;
    case DIR_DOWN:
        xx = x + ((w - sw) >> 1);
        yy = y + h;
        if (yy + sh > WinGetH(VROOT) && WinGetH(VROOT) - (y + h) < y)
        {
            dir = DIR_UP;
            yy = y - sh;
        }
        break;
    case DIR_LEFT:
        xx = x - sw;
        yy = y + ((h - sh) >> 1);
        if (xx < 0 && WinGetW(VROOT) - (x + w) > x)
        {
            dir = DIR_RIGHT;
            xx = x + w;
        }
        break;
    case DIR_RIGHT:
        xx = x + w;
        yy = y + ((h - sh) >> 1);
        if (xx + sw > WinGetW(VROOT) && WinGetW(VROOT) - (x + w) < x)
        {
            dir = DIR_LEFT;
            xx = x - sw;
        }
        break;
    default:
        break;
    }

    SlideoutArrange(s, dir);

    if (ewin)
    {
        /* If the slideout is associated with an ewin,
         * put it on the same virtual desktop. */
        dsk = EoGetDesk(ewin);
        if (BorderWinpartIndex(ewin, win) >= 0 &&
            !EoIsFloating(ewin) /* && !ewin->sticky */ )
        {
            xx -= EoGetX(dsk);
            yy -= EoGetY(dsk);
        }
        EoSetLayer(s, EoGetLayer(ewin));
    }
    else
    {
        dsk = DeskGet(0);
        EoSetLayer(s, 10);
        EoSetFloating(s, 1);
    }
    EoReparent(s, EoObj(dsk), xx, yy);

    switch (dir)
    {
    case DIR_LEFT:
        att.win_gravity = SouthEastGravity;
        EChangeWindowAttributes(EoGetWin(s), CWWinGravity, &att);
        att.win_gravity = NorthWestGravity;
        for (i = 0; i < s->num_objs; i++)
            EChangeWindowAttributes(EobjGetWin(s->objs[i]), CWWinGravity, &att);
        EoMoveResize(s, xx, yy, 1, 1);
        ESync(ESYNC_SLIDEOUT);
        EoMap(s, 2);
        EobjSlideSizeTo(EoObj(s), xx + sw, yy, xx, yy, 1, sh, sw, sh,
                        Conf.shading.speed);
        break;
    case DIR_RIGHT:
        att.win_gravity = NorthWestGravity;
        EChangeWindowAttributes(EoGetWin(s), CWWinGravity, &att);
        att.win_gravity = SouthEastGravity;
        for (i = 0; i < s->num_objs; i++)
            EChangeWindowAttributes(EobjGetWin(s->objs[i]), CWWinGravity, &att);
        EoMoveResize(s, xx, yy, 1, 1);
        ESync(ESYNC_SLIDEOUT);
        EoMap(s, 2);
        EobjSlideSizeTo(EoObj(s), xx, yy, xx, yy, 1, sh, sw, sh,
                        Conf.shading.speed);
        break;
    case DIR_UP:
        att.win_gravity = SouthEastGravity;
        EChangeWindowAttributes(EoGetWin(s), CWWinGravity, &att);
        att.win_gravity = NorthWestGravity;
        for (i = 0; i < s->num_objs; i++)
            EChangeWindowAttributes(EobjGetWin(s->objs[i]), CWWinGravity, &att);
        EoMoveResize(s, xx, yy, 1, 1);
        ESync(ESYNC_SLIDEOUT);
        EoMap(s, 2);
        EobjSlideSizeTo(EoObj(s), xx, yy + sh, xx, yy, sw, 1, sw, sh,
                        Conf.shading.speed);
        break;
    case DIR_DOWN:
        att.win_gravity = NorthWestGravity;
        EChangeWindowAttributes(EoGetWin(s), CWWinGravity, &att);
        att.win_gravity = SouthEastGravity;
        for (i = 0; i < s->num_objs; i++)
            EChangeWindowAttributes(EobjGetWin(s->objs[i]), CWWinGravity, &att);
        EoMoveResize(s, xx, yy, 1, 1);
        ESync(ESYNC_SLIDEOUT);
        EoMap(s, 2);
        EobjSlideSizeTo(EoObj(s), xx, yy, xx, yy, sw, 1, sw, sh,
                        Conf.shading.speed);
        break;
    default:
        break;
    }
    s->ref_count++;
    s->context_ewin = ewin;

    GrabPointerSet(EoGetWin(s), ECSR_ROOT, 0);

    Mode_slideouts.active = s;
}

static void
SlideoutHide(Slideout *s)
{
    if (!s)
        return;

    GrabPointerRelease();
    EoUnmap(s);
    s->context_ewin = NULL;
    s->ref_count--;
    Mode_slideouts.active = NULL;
}

static void
SlideoutButtonCallback(void *prm, XEvent *ev, ActionClass *ac)
{
    Slideout       *s = (Slideout *) prm;
    EWin           *ewin = s->context_ewin;

    if (ev->type == ButtonRelease)
        SlideoutHide(s);

    if (ac)
        ActionclassEvent(ac, ev, ewin);
}

static void
SlideoutAddButton(Slideout *s, const char *bname)
{
    Button         *b;

    if (!s)
        return;

    b = ButtonFind(bname);
    if (!b)
        return;

    s->num_objs++;
    s->objs = EREALLOC(EObj *, s->objs, s->num_objs);
    s->objs[s->num_objs - 1] = ButtonSwallowInto(b, EoObj(s));
    ButtonSetCallback(b, SlideoutButtonCallback, s);
}

#if 0
static void
SlideoutRemoveButton(Slideout *s, Button *b)
{
    s = NULL;
    b = NULL;
}
#endif

static void
SlideoutHandleEvent(Win win __UNUSED__, XEvent *ev, void *prm)
{
    Slideout       *s = (Slideout *) prm;

    switch (ev->type)
    {
    case KeyPress:
    case KeyRelease:
        SlideoutHide(s);
        break;
    case ButtonPress:
        break;
    case ButtonRelease:
        SlideoutHide(s);
        break;
    case EnterNotify:
        if (ev->xcrossing.mode != NotifyGrab)
            GrabPointerRelease();
        break;
    case LeaveNotify:
        if (ev->xcrossing.mode != NotifyUngrab)
            GrabPointerSet(EoGetWin(s), ECSR_ROOT, 0);
        break;
    }
}

static void
SlideoutsHide(void)
{
    if (Mode_slideouts.active)
        SlideoutHide(Mode_slideouts.active);
}

/*
 * Configuration load/save
 */
#include "conf.h"

int
SlideoutsConfigLoad(FILE *fs)
{
    int             err = 0;
    Slideout       *slideout = 0;
    int             i1;
    char            s[FILEPATH_LEN_MAX];
    char            s2[FILEPATH_LEN_MAX];
    char            name[FILEPATH_LEN_MAX];

    name[0] = '\0';

    while (GetLine(s, sizeof(s), fs))
    {
        i1 = ConfigParseline1(s, s2, NULL, NULL);
        switch (i1)
        {
        case CONFIG_CLOSE:
            if (slideout)
                LIST_PREPEND(Slideout, &slideout_list, slideout);
            goto done;
        case CONFIG_CLASSNAME:
            strcpy(name, s2);
            break;
        case SLIDEOUT_DIRECTION:
            slideout = SlideoutCreate(name, (char)atoi(s2));
            break;
        case CONFIG_BUTTON:
            SlideoutAddButton(slideout, s2);
            break;
        default:
            ConfigParseError("Slideout", s);
            break;
        }
    }
    err = -1;

  done:
    return err;
}

/*
 * Slideouts Module
 */

static void
SlideoutsSighan(int sig, void *prm)
{
    switch (sig)
    {
    case ESIGNAL_AREA_SWITCH_START:
    case ESIGNAL_DESK_SWITCH_START:
        SlideoutsHide();
        break;

    case ESIGNAL_EWIN_UNMAP:
        if (Mode_slideouts.active
            && Mode_slideouts.active->context_ewin == (EWin *) prm)
            SlideoutsHide();
        break;
    }
}

static int
_SlideoutMatchName(const void *data, const void *match)
{
    return strcmp(((const Slideout *)data)->name, (const char *)match);
}

static Slideout *
SlideoutFind(const char *name)
{
    return LIST_FIND(Slideout, &slideout_list, _SlideoutMatchName, name);
}

static void
IPC_Slideout(const char *params)
{
    Slideout       *s;

    if (!params)
        return;

    s = SlideoutFind(params);
    if (!s)
        return;

    SoundPlay(SOUND_SLIDEOUT_SHOW);
    SlideoutShow(s, GetContextEwin(), Mode.context_win);
}

static const IpcItem SlideoutsIpcArray[] = {
    {
     IPC_Slideout, "slideout", NULL, "Show slideout", NULL },
};

/*
 * Module descriptor
 */
extern const EModule ModSlideouts;

const EModule   ModSlideouts = {
    "slideouts", "slideout",
    SlideoutsSighan,
    MOD_ITEMS(SlideoutsIpcArray),
    { 0, NULL }
};
