/*
 *  Window Maker window manager
 *
 *  Copyright (c) 1997      Shige Abe
 *  Copyright (c) 1997-2003 Alfredo K. Kojima
 *
 *  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 2 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 <https://www.gnu.org/licenses/>.
 */

#include "wconfig.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include "WindowMaker.h"
#include "window.h"
#include "actions.h"
#include "client.h"
#include "misc.h"
#include "stacking.h"
#include "workspace.h"
#include "framewin.h"
#include "switchmenu.h"

#define IS_GNUSTEP_MENU(w) ((w)->wm_gnustep_attr && \
	((w)->wm_gnustep_attr->flags & GSWindowLevelAttr) && \
	((w)->wm_gnustep_attr->window_level == WMMainMenuWindowLevel || \
	 (w)->wm_gnustep_attr->window_level == WMSubmenuWindowLevel))

#define MAX_RTEXT_LENGTH (MAX_WORKSPACENAME_WIDTH + MAX_SHORTCUT_LENGTH + 16)

static int initialized = 0;
static void observer(void *self, WMNotification * notif);
static void wsobserver(void *self, WMNotification * notif);

static WPixmap *switchMenuIconForWindow(WScreen *scr, WWindow *wwin)
{
	RImage *image = NULL;
	WPixmap *pix;
	WApplication *wapp;
	int max_size;

	if (!scr || !wwin)
		return NULL;

	max_size = WMFontHeight(scr->menu_entry_font) + 2;
	if (max_size < 12)
		max_size = 12;

	/* Prefer the actual appicon image when available */
	wapp = wApplicationOf(wwin->main_window);
	if (wapp && wapp->app_icon && wapp->app_icon->icon && wapp->app_icon->icon->file_image) {
		image = RRetainImage(wapp->app_icon->icon->file_image);
	}

	/* Fall back to _NET_WM_ICON, then the default icon */
	if (!image && !WFLAGP(wwin, always_user_icon) && wwin->net_icon_image)
		image = RRetainImage(wwin->net_icon_image);
	if (!image)
		image = get_icon_image(scr, wwin->wm_instance, wwin->wm_class, max_size);

	if (!image)
		return NULL;

	image = wIconValidateIconSize(image, max_size);
	if (!image)
		return NULL;

	pix = wmalloc(sizeof(WPixmap));
	memset(pix, 0, sizeof(WPixmap));
	RConvertImageMask(scr->rcontext, image, &pix->image, &pix->mask, 128);
	pix->width = image->width;
	pix->height = image->height;
	pix->depth = scr->w_depth;

	RReleaseImage(image);
	return pix;
}

/*
 * FocusWindow
 *
 *  - Needs to check if already in the right workspace before
 *    calling wChangeWorkspace?
 *
 *  Order:
 *    Switch to correct workspace
 *    Unshade if shaded
 *    If iconified then deiconify else focus/raise.
 */
static void focusWindow(WMenu * menu, WMenuEntry * entry)
{
	WWindow *wwin;

	/* Parameter not used, but tell the compiler that it is ok */
	(void) menu;

	wwin = (WWindow *) entry->clientdata;
	wWindowSingleFocus(wwin);
}

void InitializeSwitchMenu(void)
{
	if (!initialized) {
		initialized = 1;

		WMAddNotificationObserver(observer, NULL, WMNManaged, NULL);
		WMAddNotificationObserver(observer, NULL, WMNUnmanaged, NULL);
		WMAddNotificationObserver(observer, NULL, WMNChangedWorkspace, NULL);
		WMAddNotificationObserver(observer, NULL, WMNChangedState, NULL);
		WMAddNotificationObserver(observer, NULL, WMNChangedFocus, NULL);
		WMAddNotificationObserver(observer, NULL, WMNChangedStacking, NULL);
		WMAddNotificationObserver(observer, NULL, WMNChangedName, NULL);

		WMAddNotificationObserver(wsobserver, NULL, WMNWorkspaceChanged, NULL);
		WMAddNotificationObserver(wsobserver, NULL, WMNWorkspaceNameChanged, NULL);
	}
}

/*
 *
 * Open switch menu
 *
 */
void OpenSwitchMenu(WScreen * scr, int x, int y, int keyboard)
{
	WMenu *switchmenu = scr->switch_menu;
	WWindow *wwin;

	if (switchmenu) {
		if (switchmenu->flags.mapped) {
			if (!switchmenu->flags.buttoned) {
				wMenuUnmap(switchmenu);
			} else {
				wRaiseFrame(switchmenu->frame->core);

				if (keyboard)
					wMenuMapAt(switchmenu, 0, 0, True);
				else
					wMenuMapCopyAt(switchmenu, x - switchmenu->frame->core->width / 2, y);
			}
		} else {
			if (keyboard && x == scr->scr_width / 2 && y == scr->scr_height / 2) {
				y = y - switchmenu->frame->core->height / 2;
			}
			wMenuMapAt(switchmenu, x - switchmenu->frame->core->width / 2, y, keyboard);
		}
		return;
	}
	switchmenu = wMenuCreate(scr, _("Windows"), True);
	scr->switch_menu = switchmenu;

	wwin = scr->focused_window;
	while (wwin) {
		UpdateSwitchMenu(scr, wwin, ACTION_ADD);

		wwin = wwin->prev;
	}

	if (switchmenu) {
		int newx, newy;

		if (!switchmenu->flags.realized)
			wMenuRealize(switchmenu);

		if (keyboard && x == 0 && y == 0) {
			newx = newy = 0;
		} else if (keyboard && x == scr->scr_width / 2 && y == scr->scr_height / 2) {
			newx = x - switchmenu->frame->core->width / 2;
			newy = y - switchmenu->frame->core->height / 2;
		} else {
			newx = x - switchmenu->frame->core->width / 2;
			newy = y;
		}
		wMenuMapAt(switchmenu, newx, newy, keyboard);
	}
}

static int menuIndexForWindow(WMenu * menu, WWindow * wwin, int old_pos)
{
	int idx, move_down;

	if (menu->entry_no <= old_pos)
		return -1;

	for (idx = 0, move_down = 0; idx < menu->entry_no; idx++) {
		WWindow *tw = (WWindow *) menu->entries[idx]->clientdata;

		/* Is the window moving down in the menu?  If so, we'll need to
		   adjust its new index by 1. */
		if (tw == wwin)
			move_down = 1;

		if (IS_OMNIPRESENT(tw) || (tw != wwin &&
					   tw->frame->workspace >= wwin->frame->workspace))
			return idx - move_down;
	}

	return idx;
}

static void fillRtext(char *buf, size_t bufsz, WWindow *wwin, WScreen *scr)
{
	char *mlbl = wwin->mark_key_label ? GetShortcutString(wwin->mark_key_label) : NULL;

	if (IS_OMNIPRESENT(wwin)) {
		if (mlbl)
			snprintf(buf, bufsz, "[%s] [*]", mlbl);
		else
			snprintf(buf, bufsz, "[*]");
	} else {
		if (mlbl)
			snprintf(buf, bufsz, "[%s] [%s]", mlbl,
				 scr->workspaces[wwin->frame->workspace]->name);
		else
			snprintf(buf, bufsz, "[%s]",
				 scr->workspaces[wwin->frame->workspace]->name);
	}
	wfree(mlbl);
}

/*
 * Update switch menu
 */
void UpdateSwitchMenu(WScreen * scr, WWindow * wwin, int action)
{
	WMenu *switchmenu = scr->switch_menu;
	WMenuEntry *entry;
	char title[MAX_MENU_TEXT_LENGTH + 6];
	int len = sizeof(title);
	int i;
	int checkVisibility = 0;

	if (!wwin->screen_ptr->switch_menu)
		return;
	/*
	 *  This menu is updated under the following conditions:
	 *
	 *    1.  When a window is created.
	 *    2.  When a window is destroyed.
	 *
	 *    3.  When a window changes its title.
	 *    4.  When a window changes its workspace.
	 */
	if (action == ACTION_ADD) {
		char *t;
		int idx;

		if (wwin->flags.internal_window || WFLAGP(wwin, skip_window_list) || IS_GNUSTEP_MENU(wwin)) {
			return;
		}

		if (wwin->frame->title)
			snprintf(title, len, "%s", wwin->frame->title);
		else
			snprintf(title, len, "%s", DEF_WINDOW_TITLE);
		t = ShrinkString(scr->menu_entry_font, title, MAX_WINDOWLIST_WIDTH);

		if (IS_OMNIPRESENT(wwin))
			idx = -1;
		else {
			idx = menuIndexForWindow(switchmenu, wwin, 0);
		}

		entry = wMenuInsertCallback(switchmenu, idx, t, focusWindow, wwin);
		wfree(t);

		entry->icon = switchMenuIconForWindow(scr, wwin);

		entry->flags.indicator = 1;
		entry->rtext = wmalloc(MAX_RTEXT_LENGTH);
		fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);

		if (wwin->flags.hidden) {
			entry->flags.indicator_type = MI_HIDDEN;
			entry->flags.indicator_on = 1;
		} else if (wwin->flags.miniaturized) {
			entry->flags.indicator_type = MI_MINIWINDOW;
			entry->flags.indicator_on = 1;
		} else if (wwin->flags.focused) {
			entry->flags.indicator_type = MI_DIAMOND;
			entry->flags.indicator_on = 1;
		} else if (wwin->flags.shaded) {
			entry->flags.indicator_type = MI_SHADED;
			entry->flags.indicator_on = 1;
		}

		wMenuRealize(switchmenu);
		checkVisibility = 1;
	} else {
		char *t;
		for (i = 0; i < switchmenu->entry_no; i++) {
			entry = switchmenu->entries[i];
			/* this is the entry that was changed */
			if (entry->clientdata == wwin) {
				switch (action) {
				case ACTION_REMOVE:
					wMenuRemoveItem(switchmenu, i);
					wMenuRealize(switchmenu);
					checkVisibility = 1;
					break;

				case ACTION_CHANGE:
					if (entry->text)
						wfree(entry->text);

					if (wwin->frame->title)
						snprintf(title, MAX_MENU_TEXT_LENGTH, "%s", wwin->frame->title);
					else
						snprintf(title, MAX_MENU_TEXT_LENGTH, "%s", DEF_WINDOW_TITLE);

					t = ShrinkString(scr->menu_entry_font, title, MAX_WINDOWLIST_WIDTH);
					entry->text = t;

					fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);

					wMenuRealize(switchmenu);
					checkVisibility = 1;
					break;

				case ACTION_CHANGE_WORKSPACE:
					if (entry->rtext) {
						int idx = -1;
						char *t, *rt;
						WPixmap *ipix;
						int it, ion;

						fillRtext(entry->rtext, MAX_RTEXT_LENGTH, wwin, scr);

						rt = entry->rtext;
						entry->rtext = NULL;
						ipix = entry->icon;
						entry->icon = NULL;
						t = entry->text;
						entry->text = NULL;

						it = entry->flags.indicator_type;
						ion = entry->flags.indicator_on;

						if (!IS_OMNIPRESENT(wwin) && idx < 0) {
							idx = menuIndexForWindow(switchmenu, wwin, i);
						}

						wMenuRemoveItem(switchmenu, i);

						entry = wMenuInsertCallback(switchmenu, idx, t, focusWindow, wwin);
						wfree(t);
						entry->rtext = rt;
						entry->icon = ipix;
						entry->flags.indicator = 1;
						entry->flags.indicator_type = it;
						entry->flags.indicator_on = ion;
					}
					wMenuRealize(switchmenu);
					checkVisibility = 1;
					break;

				case ACTION_CHANGE_STATE:
					if (wwin->flags.hidden) {
						entry->flags.indicator_type = MI_HIDDEN;
						entry->flags.indicator_on = 1;
					} else if (wwin->flags.miniaturized) {
						entry->flags.indicator_type = MI_MINIWINDOW;
						entry->flags.indicator_on = 1;
					} else if (wwin->flags.shaded && !wwin->flags.focused) {
						entry->flags.indicator_type = MI_SHADED;
						entry->flags.indicator_on = 1;
					} else {
						entry->flags.indicator_on = wwin->flags.focused;
						entry->flags.indicator_type = MI_DIAMOND;
					}
					break;
				}
				break;
			}
		}
	}
	if (checkVisibility) {
		int tmp;

		tmp = switchmenu->frame->top_width + 5;
		/* if menu got unreachable, bring it to a visible place */
		if (switchmenu->frame_x < tmp - (int)switchmenu->frame->core->width) {
			wMenuMove(switchmenu, tmp - (int)switchmenu->frame->core->width,
				  switchmenu->frame_y, False);
		}
	}
	wMenuPaint(switchmenu);
}

static void UpdateSwitchMenuWorkspace(WScreen *scr, int workspace)
{
	WMenu *menu = scr->switch_menu;
	int i;
	WWindow *wwin;

	if (!menu)
		return;

	for (i = 0; i < menu->entry_no; i++) {
		wwin = (WWindow *) menu->entries[i]->clientdata;

		if (wwin->frame->workspace == workspace && !IS_OMNIPRESENT(wwin)) {
			fillRtext(menu->entries[i]->rtext, MAX_RTEXT_LENGTH, wwin, scr);
			menu->flags.realized = 0;
		}
	}
	if (!menu->flags.realized)
		wMenuRealize(menu);
}

static void observer(void *self, WMNotification * notif)
{
	WWindow *wwin = (WWindow *) WMGetNotificationObject(notif);
	const char *name = WMGetNotificationName(notif);
	void *data = WMGetNotificationClientData(notif);

	/* Parameter not used, but tell the compiler that it is ok */
	(void) self;

	if (!wwin)
		return;

	if (strcmp(name, WMNManaged) == 0)
		UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_ADD);
	else if (strcmp(name, WMNUnmanaged) == 0)
		UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_REMOVE);
	else if (strcmp(name, WMNChangedWorkspace) == 0)
		UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE_WORKSPACE);
	else if (strcmp(name, WMNChangedFocus) == 0)
		UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE_STATE);
	else if (strcmp(name, WMNChangedName) == 0)
		UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE);
	else if (strcmp(name, WMNChangedState) == 0) {
		if (strcmp((char *)data, "omnipresent") == 0) {
			UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE_WORKSPACE);
		} else {
			UpdateSwitchMenu(wwin->screen_ptr, wwin, ACTION_CHANGE_STATE);
		}
	}
}

static void wsobserver(void *self, WMNotification * notif)
{
	WScreen *scr = (WScreen *) WMGetNotificationObject(notif);
	const char *name = WMGetNotificationName(notif);
	void *data = WMGetNotificationClientData(notif);

	/* Parameter not used, but tell the compiler that it is ok */
	(void) self;

	if (strcmp(name, WMNWorkspaceNameChanged) == 0) {
		UpdateSwitchMenuWorkspace(scr, (uintptr_t)data);
	} else if (strcmp(name, WMNWorkspaceChanged) == 0) {

	}
}
