/*
 * Copyright (C) 2005 Fabian Yamaguchi <fabiany at gmx.net>
 *
 * 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#define LOCAL_DEBUG
#define EVENT_TRACE

#include "../configure.h"
#include "asapp.h"

#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <regex.h>
#include <string.h>
#include <stdio.h>

#include "../libAfterImage/afterimage.h"

#include "afterstep.h"
#include "screen.h"
#include "module.h"
#include "mystyle.h"
#include "mystyle_property.h"
#include "parser.h"
#include "clientprops.h"
#include "wmprops.h"
#include "decor.h"
#include "aswindata.h"
#include "balloon.h"
#include "event.h"
#include "shape.h"

#include "../libAfterBase/aslist.h"
#include "../libAfterBase/ashash.h"


#include "ascommand.h"
#include "operations.h"

/* local definitions */

typedef void (*WinC_handler) (ASWindowData * wd, void *data);

typedef struct {
	Window cl;
} client_item;

char *DEFAULT_PATTERN = "";
Bool selection_in_progress = False;


ASBiDirList *extract_matches (ASBiDirList * src, const char *pattern);
Bool apply_operations (void *data, void *aux_data);
void destroy_client_item (void *data);


ASASCommandState ASCommandState;

/****************** private *************************/

void DeadPipe (int nonsense)
{
	static int already_dead = False;

	if (already_dead)
		return;											/* non-reentrant function ! */
	already_dead = True;

	window_data_cleanup ();

	FreeMyAppResources ();

#ifdef DEBUG_ALLOCS
	print_unfreed_mem ();
#endif													/* DEBUG_ALLOCS */

	XFlush (dpy);									/* need this for SetErootPixmap to take effect */
	XCloseDisplay (dpy);					/* need this for SetErootPixmap to take effect */
	exit (0);
}


/****************************************************************************/
/* PROCESSING OF AFTERSTEP MESSAGES :                                       */
/****************************************************************************/
Bool process_message (send_data_type type, send_data_type * body)
{
	client_item *new_item;
	WindowPacketResult res;
	ASBiDirElem *curr;

	LOCAL_DEBUG_OUT ("received message %lX", type);

	if (type == M_END_WINDOWLIST) {

		return True;

	} else if ((type & WINDOW_PACKET_MASK) != 0) {
		struct ASWindowData *wd = fetch_window_by_id (body[0]);

		if ((res = handle_window_packet (type, body, &wd)) == WP_DataCreated) {
			new_item = safemalloc (sizeof (client_item));
			new_item->cl = wd->client;
			append_bidirelem (ASCommandState.clients_order, new_item);

		} else if (res == WP_DataDeleted) {
			/* Delete element */
			for (curr = ASCommandState.clients_order->head; curr != NULL;
					 curr = curr->next) {
				if (((client_item *) (curr->data))->cl == wd->client)
					destroy_bidirelem (ASCommandState.clients_order, curr);
			}

		}

	} else if (type == M_NEW_DESKVIEWPORT) {
		LOCAL_DEBUG_OUT ("M_NEW_DESKVIEWPORT(desk = %ld,Vx=%ld,Vy=%ld)",
										 body[2], body[0], body[1]);
		Scr.CurrentDesk = body[2];
		Scr.Vx = body[0];
		Scr.Vy = body[1];
	}

	return False;
}

Bool apply_operations (void *data, void *aux_data)
{
	ASWindowData *wd = fetch_window_by_id (((client_item *) data)->cl);
	ASBiDirElem *curr;
	void *h;

	for (curr = (ASCommandState.operations)->head; curr != NULL;
			 curr = curr->next) {
		/* If lookup wasn't successful, move along */
		if (get_hash_item
				(ASCommandState.handlers, AS_HASHABLE (curr->data),
				 &h) != ASH_Success) {
			LOCAL_DEBUG_OUT ("handler %s not found", (char *)curr->data);
			continue;
		}
		LOCAL_DEBUG_OUT ("executing handler %s for %s", (char *)curr->data,
										 wd->window_name);
		((WinC_handler) h) (wd, aux_data);
	}
	return True;
}

Bool fix_area (void *data, void *list)
{
	ASWindowData *wd = fetch_window_by_id (((client_item *) data)->cl);

	if (wd == NULL)
		return True;

	if (get_flags (wd->flags, AS_AvoidCover)
			&& !get_flags (wd->state_flags, AS_Iconic)) {
		subtract_rectangle_from_list ((ASVector *) list, wd->frame_rect.x,
																	wd->frame_rect.y,
																	wd->frame_rect.x +
																	(int)wd->frame_rect.width,
																	wd->frame_rect.y +
																	(int)wd->frame_rect.height);
	}
	return True;
}

void destroy_client_item (void *data)
{
	free ((client_item *) data);
}


/****************** /private ************************/


/*************** Public ****************/

/* Initialize ascom. A connection to the afterstep-sever
 * is established and all handlers are registered
 */

void ascom_init (void)
{

	set_DeadPipe_handler (DeadPipe);
	set_signal_handler (SIGSEGV);


	memset (&ASCommandState, 0x00, sizeof (ASCommandState));

	ASCommandState.selected_wins = create_asbidirlist (NULL);
	ASCommandState.clients_order = create_asbidirlist (destroy_client_item);
	ASCommandState.operations = create_asbidirlist (free);

	ASCommandState.handlers =
			create_ashash (7, string_hash_value, string_compare,
										 string_destroy_without_data);

	/* Register handlers */
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("move")),
								 move_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("resize")),
								 resize_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("kill")),
								 kill_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("jump")),
								 jump_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("ls")),
								 ls_handler);
	add_hash_item (ASCommandState.handlers,
								 AS_HASHABLE (strdup ("deiconify")), deiconify_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("iconify")),
								 iconify_handler);
	add_hash_item (ASCommandState.handlers,
								 AS_HASHABLE (strdup ("sendtodesk")),
								 send_to_desk_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("center")),
								 center_handler);
	add_hash_item (ASCommandState.handlers, AS_HASHABLE (strdup ("raise")),
								 raise_handler);

	ConnectAfterStep (WINDOW_CONFIG_MASK | WINDOW_NAME_MASK |
										M_END_WINDOWLIST | M_NEW_DESKVIEWPORT, 0);

}


/*
  Clean up.
*/

void ascom_deinit (void)
{
	destroy_ashash (&(ASCommandState.handlers));

	destroy_asbidirlist (&(ASCommandState.operations));
	destroy_asbidirlist (&(ASCommandState.selected_wins));
	destroy_asbidirlist (&(ASCommandState.clients_order));
}



void ascom_update_winlist (void)
{
	Bool complete = False;

	clear_selection ();
	SendInfo ("Send_WindowList", 0);

	while (!complete) {
		ASMessage *msg = CheckASMessage (WAIT_AS_RESPONSE_TIMEOUT);

		if (msg) {
			complete = process_message (msg->header[1], msg->body);
			DestroyASMessage (msg);
		}
	}

}

/* run ascom_update_winlist first */
char **ascom_get_win_names (void)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	int n_names = 0;
	char **ret = NULL;
	int i = 0;

	if (ASCommandState.clients_order->head == NULL)
		return NULL;

	for (curr = ASCommandState.clients_order->head; curr != NULL;
			 curr = curr->next)
		n_names++;

	ret = safemalloc (sizeof (char *) * n_names + 1);

	for (curr = ASCommandState.clients_order->head; curr != NULL;
			 curr = curr->next) {
		wd = fetch_window_by_id (((client_item *) curr->data)->cl);
		ret[i++] = strdup (wd->window_name);
	}

	ret[n_names] = NULL;

	return ret;
}

XRectangle *ascom_get_available_area (void)
{
	ASVector *list = create_asvector (sizeof (XRectangle));
	XRectangle seed_rect, *result_rect = NULL;
	int i, largest = 0;
	XRectangle *rects;

	/* must seed the list with the single rectangle representing the area : */
	seed_rect.x = 0;
	seed_rect.y = 0;
	seed_rect.width = Scr.MyDisplayWidth;
	seed_rect.height = Scr.MyDisplayHeight;

	append_vector (list, &seed_rect, 1);

	iterate_asbidirlist (ASCommandState.clients_order, fix_area, list, NULL,
											 False);

	print_rectangles_list (list);

	i = PVECTOR_USED (list);
	rects = PVECTOR_HEAD (XRectangle, list);
	while (--i > 0) {
		if (rects[largest].width * rects[largest].height <
				rects[i].width * rects[i].height)
			largest = i;
	}

	result_rect = safemalloc (sizeof (XRectangle));
	*result_rect = rects[largest];

	destroy_asvector (&list);

	return result_rect;

}

void parse_op_string (const char *op)
{
	char *iter, *haystack, *copy;

	destroy_asbidirlist (&ASCommandState.operations);
	ASCommandState.operations = create_asbidirlist (free);

	copy = haystack = strdup (op);
	while ((iter = strtok (haystack, " "))) {
		LOCAL_DEBUG_OUT ("Adding operation: %s", iter);
		append_bidirelem (ASCommandState.operations, strdup (iter));
		haystack = NULL;
	}

	free (copy);
}

/*
  op can contain an unlimited amount of
  commands separated by whitespace. These
  commands are then executed in order.
*/
void ascom_do (const char *op, void *data)
{


	LOCAL_DEBUG_OUT ("ascom_do called: op = %s", op);

	parse_op_string (op);

	if (ASCommandState.selected_wins->head == NULL) {
		LOCAL_DEBUG_OUT ("No windows were selected.");
		return;
	}

	iterate_asbidirlist (ASCommandState.selected_wins, apply_operations,
											 data, NULL, False);

}


/* peform operation on only the first/last window */
void ascom_do_one (const char *op, void *data, Bool last)
{
	client_item *elem;

	if (!ASCommandState.selected_wins->head) {
		LOCAL_DEBUG_OUT ("No windows selected");
		return;
	}

	if (last)
		elem = ASCommandState.selected_wins->tail->data;
	else
		elem = ASCommandState.selected_wins->head->data;

	if (!elem)
		return;

	parse_op_string (op);
	apply_operations (elem, data);

}

/* Remove the first/last element from the list
 * of selected windows. */
void ascom_pop_winlist (Bool last)
{
	if (last)
		extract_last_bidirelem (ASCommandState.selected_wins);
	else
		extract_first_bidirelem (ASCommandState.selected_wins);
}


const ASWindowData *ascom_get_next_window (Bool last)
{
	if (last)
		return
				fetch_window_by_id (((client_item
															*) (ASCommandState.selected_wins->tail->
																	data))->cl);
	else
		return
				fetch_window_by_id (((client_item
															*) (ASCommandState.selected_wins->head->
																	data))->cl);
}

Bool winlist_is_empty (void)
{
	return (ASCommandState.selected_wins->head == NULL);
}

void ascom_set_flag (ASFlagType fl)
{
	set_flags (ASCommandState.flags, fl);
}

/*
  Before exiting, make sure to call this function.
  This'll make sure all commands have been sent
  to the server.
*/

void ascom_wait (void)
{
	/* Hack: Request another window-list. Next time we
	 * receive M_END_WINDOWLIST we can be sure all of our
	 * move/resize/whatever commands have been executed and
	 * it's safe to die. */
	ascom_update_winlist ();
}


/* Selection-functions */
void select_all (Bool unselect)
{
	ASBiDirElem *curr;
	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {
		if (!unselect)
			append_bidirelem (new_selection, curr->data);
	}

	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;

	selection_in_progress = True;
}

Bool
select_windows_by_pattern (const char *pattern, Bool just_one,
													 Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	regex_t my_reg;
	int ret;

	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (regcomp (&my_reg, pattern, REG_EXTENDED | REG_ICASE) != 0) {
		LOCAL_DEBUG_OUT ("Error compiling regex");
		return False;
	}

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {
		wd = fetch_window_by_id (((client_item *) curr->data)->cl);


		ret = regexec (&my_reg, wd->window_name, 0, NULL, 0);
		if (((ret == 0) && !unselect) || ((ret != 0) && unselect)) {

			append_bidirelem (new_selection, curr->data);

			if (new_selection->head && just_one)
				break;

		}

	}


	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	regfree (&my_reg);
	selection_in_progress = True;
	return True;
}

void select_windows_on_screen (Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {

		wd = fetch_window_by_id (((client_item *) curr->data)->cl);


		if (unselect) {

			/* add this window if it's not on current screen or desk */
			if ((wd->desk != Scr.CurrentDesk) || ((wd->frame_rect.x < Scr.Vx)
																						|| (wd->frame_rect.y < Scr.Vy)
																						|| (wd->frame_rect.x >
																								(Scr.MyDisplayWidth +
																								 Scr.Vx))
																						|| (wd->frame_rect.y >
																								(Scr.MyDisplayHeight +
																								 Scr.Vy))))
				append_bidirelem (new_selection, curr->data);

		} else {
			/* skip this window if it's not on current-desk. */
			if ((wd->desk != Scr.CurrentDesk))
				continue;

			/* skip this window if it's not on current-screen */
			if (((wd->frame_rect.x < Scr.Vx)
					 || (wd->frame_rect.y < Scr.Vy)
					 || (wd->frame_rect.x > (Scr.MyDisplayWidth + Scr.Vx))
					 || (wd->frame_rect.y > (Scr.MyDisplayHeight + Scr.Vy))))
				continue;

			append_bidirelem (new_selection, curr->data);
		}

	}

	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}

void select_windows_on_desk (Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	ASBiDirList *new_selection = create_asbidirlist (NULL);


	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;


	for (; curr != NULL; curr = curr->next) {

		wd = fetch_window_by_id (((client_item *) curr->data)->cl);

		if ((unselect && (wd->desk != Scr.CurrentDesk))
				|| (!unselect && (wd->desk == Scr.CurrentDesk)))
			append_bidirelem (new_selection, curr->data);

	}

	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}

void select_windows_by_flag (ASFlagType flag, Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	Bool ret;

	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {
		wd = fetch_window_by_id (((client_item *) curr->data)->cl);


		ret = get_flags (wd->flags, flag);

		if ((ret && !unselect) || (!ret && unselect)) {

			append_bidirelem (new_selection, curr->data);
		}

	}


	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}


void select_windows_by_state_flag (ASFlagType flag, Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	Bool ret;

	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {
		wd = fetch_window_by_id (((client_item *) curr->data)->cl);


		ret = get_flags (wd->state_flags, flag);

		if ((ret && !unselect) || (!ret && unselect)) {

			append_bidirelem (new_selection, curr->data);
		}

	}


	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}

void select_untitled_windows (Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {

		wd = fetch_window_by_id (((client_item *) curr->data)->cl);

		if ((unselect && wd->window_name) || (!unselect && !(wd->window_name)))
			append_bidirelem (new_selection, curr->data);

	}

	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}

int num_selected_windows (void)
{
	return (int)ASCommandState.selected_wins->count;
}

/* not working right now */
void select_focused_window (Bool unselect)
{
	ASBiDirElem *curr;
	ASWindowData *wd;
	ASBiDirList *new_selection = create_asbidirlist (NULL);

	if (selection_in_progress)
		curr = ASCommandState.selected_wins->head;
	else
		curr = ASCommandState.clients_order->head;

	for (; curr != NULL; curr = curr->next) {

		wd = fetch_window_by_id (((client_item *) curr->data)->cl);

		if ((wd->focused && !unselect) || (!wd->focused && unselect)) {

			append_bidirelem (new_selection, curr->data);
			destroy_asbidirlist (&ASCommandState.selected_wins);
			ASCommandState.selected_wins = new_selection;
			selection_in_progress = True;
			return;
		}

	}

	destroy_asbidirlist (&ASCommandState.selected_wins);
	ASCommandState.selected_wins = new_selection;
	selection_in_progress = True;
}


void clear_selection (void)
{
	destroy_asbidirlist (&ASCommandState.selected_wins);
	selection_in_progress = False;
}

/****************** /public **********************/
