/* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-

   AZKeyboardHandler.m for the Azalea window manager
   Copyright (c) 2006        Yen-Ju Chen
	 
   keyboard.c for the Openbox window manager
   Copyright (c) 2004        Mikael Magnusson
   Copyright (c) 2003        Ben Jansens

   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.

   See the COPYING file for a copy of the GNU General Public License.
*/

#import "AZMainLoop.h"
#import "AZScreen.h"
#import "AZClientManager.h"
#import "openbox.h"
#import "grab.h"
#import "action.h"
#import "prop.h"
#import "config.h"
#import "AZKeyTree.h"
#import "AZKeyboardHandler.h"
#import "translate.h"

@interface AZInteractiveState: NSObject
{
    unsigned int state;
    AZClient *client;
    NSArray *actions;
    ObFrameContext context;
}
- (unsigned int) state;
- (AZClient *) client;
- (NSArray *) actions;
- (ObFrameContext) context;
- (void) set_state: (unsigned int) state;
- (void) set_client:(AZClient *) client;
- (void) set_actions: (NSArray *) actions;
- (void) set_context: (ObFrameContext) context;
@end

@implementation AZInteractiveState
- (unsigned int) state { return state; }
- (AZClient *) client { return client; }
- (NSArray *) actions { return actions; }
- (ObFrameContext) context { return context; }
- (void) set_state: (unsigned int) s { state = s; }
- (void) set_client:(AZClient *) c { client = c; }
- (void) set_actions: (NSArray *) a { ASSIGN(actions, a); }
- (void) set_context: (ObFrameContext) c { context = c; }
- (void) dealloc
{
  DESTROY(actions);
  [super dealloc];
}
@end

static AZKeyboardHandler *sharedInstance;

@interface AZKeyboardHandler (AZPrivate)
- (void) grab: (BOOL) grab forWindow: (Window) win;
- (void) grabKeys: (BOOL) grab;
- (void) interactiveEnd: (AZInteractiveState *) s state: (unsigned int) state cancel: (BOOL) cancel;
- (void) clientDestroy: (NSNotification *) not;
/* callback */
- (BOOL) chainTimeout: (id) data;

@end

@implementation AZKeyboardHandler

- (void) grab: (BOOL) grab forClient: (AZClient *) client
{
    [self grab: grab forWindow: [client window]];
}

- (void) resetChains
{
    [[AZMainLoop mainLoop] removeTimeout: self 
	                         handler: @selector(chainTimeout:)];

    if (curpos) {
        [self grabKeys: NO];
        curpos = NULL;
	[self grabKeys: YES];
    }
}

- (void) unbindAll
{
    tree_destroy(keyboard_firstnode);
    keyboard_firstnode = nil;
    [self grabKeys: NO];
    curpos = nil;
}

- (BOOL) bind: (NSArray *) keylist action: (AZAction *) action
{
    AZKeyBindingTree *tree, *t;
    BOOL conflict;
    BOOL mods = YES;

    NSAssert(keylist != nil, @"keylist is nil");
    NSAssert(action != nil, @"action is NULL");

    if (!(tree = tree_build(keylist)))
        return NO;

    if ((t = tree_find(tree, &conflict)) != NULL) {
        /* already bound to something, use the existing tree */
        tree_destroy(tree);
        tree = nil;
    } else
        t = tree;

    if (conflict) {
	NSLog(@"Warning: conflict with binding");
        tree_destroy(tree);
        return NO;
    }

    /* find if every key in this chain has modifiers, and also find the
       bottom node of the tree */
    while ([t first_child]) {
        if (![t state])
            mods = NO;
        t = [t first_child];
    }

    /* when there are no modifiers in the binding, then the action cannot
       be interactive */
    if (!mods && [action data].any.interactive) {
        [action data_pointer]->any.interactive = NO;
        [action data_pointer]->inter.final = YES;
    }

    /* set the action */
    [t addAction: action];
    /* assimilate this built tree into the main tree. assimilation
       destroys/uses the tree */
    if (tree) tree_assimilate(tree);

    return YES;
}

- (BOOL) interactiveGrab: (unsigned int) state
                  client: (AZClient *) client
                  action: (AZAction *) action
{
    AZInteractiveState *s;

    NSAssert([action data].any.interactive, @"interactive is NULL");

    if ([interactive_states count] == 0) {
        if (!grab_keyboard(YES))
            return NO;
        if (!grab_pointer(YES, OB_CURSOR_NONE)) {
            grab_keyboard(NO);
            return NO;
        }
    }

    s = [[AZInteractiveState alloc] init];

    [s set_state: state];
    [s set_client: client];
    [s set_actions: [NSArray arrayWithObjects: action, nil]];

    [interactive_states addObject: s];
    DESTROY(s);

    return YES;
}

- (BOOL) processInteractiveGrab: (XEvent *) e
                      forClient: (AZClient **) client
{
    BOOL handled = NO;
    BOOL done = NO;
    BOOL cancel = NO;
    int i;

    for (i = 0; i < [interactive_states count]; i++) {
        AZInteractiveState *s = [interactive_states objectAtIndex: i];

        if ((e->type == KeyRelease && 
             !([s state] & e->xkey.state)))
            done = YES;
        else if (e->type == KeyPress) {
            /*if (e->xkey.keycode == ob_keycode(OB_KEY_RETURN))
                done = YES;
            else */if (e->xkey.keycode == ob_keycode(OB_KEY_ESCAPE))
                cancel = done = YES;
        }
        if (done) {
	    [self interactiveEnd: s
		    state: e->xkey.state
		    cancel: cancel];

            handled = YES;
	    i--; /* s is removed */
        } else
            *client = [s client];
    }

    return handled;
}

- (void) processEvent: (XEvent *) e forClient: (AZClient *) client
{
    AZKeyBindingTree *p;

    NSAssert(e->type == KeyPress, @"Event type is not KeyPress");

    if (e->xkey.keycode == config_keyboard_reset_keycode &&
        e->xkey.state == config_keyboard_reset_state)
    {
	[self resetChains];
        return;
    }

    if (curpos == NULL)
        p = keyboard_firstnode;
    else
        p = [curpos first_child];
    while (p) {
        if ([p key] == e->xkey.keycode &&
            [p state] == e->xkey.state)
        {
            if ([p first_child] != nil) { /* part of a chain */
		AZMainLoop *mainLoop = [AZMainLoop mainLoop];
		[mainLoop removeTimeout: self 
			        handler: @selector(chainTimeout:)];
                /* 5 second timeout for chains */
		[mainLoop addTimeout: self 
			     handler: @selector(chainTimeout:)
			     microseconds: 5 * USEC_PER_SEC
			     data: nil notify: NULL];
		[self grabKeys: NO];
                curpos = p;
		[self grabKeys: YES];
            } else {

		[self resetChains];

                action_run_key([p actions], client, e->xkey.state,
                               e->xkey.x_root, e->xkey.y_root);
            }
            break;
        }
        p = [p next_sibling];
    }
}

- (BOOL) interactivelyGrabbed
{
    return ([interactive_states count] > 0);
}

- (void) startup: (BOOL) reconfig
{
    [self grabKeys: YES];

    if (!reconfig) {
	[[NSNotificationCenter defaultCenter] addObserver: self
		selector: @selector(clientDestroy:)
		name: AZClientDestroyNotification
		object: nil];
    }

    interactive_states = [[NSMutableArray alloc] init];
}

- (void) shutdown: (BOOL) reconfig
{
    if (!reconfig) {
	[[NSNotificationCenter defaultCenter] removeObserver: self];
    }

    DESTROY(interactive_states);

    [[AZMainLoop mainLoop] removeTimeout: self 
	                         handler: @selector(chainTimeout:)];

    [self unbindAll];
}

- (AZKeyBindingTree *) firstnode
{
  return keyboard_firstnode;
}

- (void) set_firstnode: (AZKeyBindingTree *) first
{
  keyboard_firstnode = first;
}

+ (AZKeyboardHandler *) defaultHandler
{
  if (sharedInstance == nil)
    sharedInstance = [[AZKeyboardHandler alloc] init];
  return sharedInstance;
}

@end

@implementation AZKeyboardHandler (AZPrivate)
- (void) grab: (BOOL) grab forWindow: (Window) win
{
    AZKeyBindingTree *p;

    ungrab_all_keys(win);

    if (grab) {
        p = curpos ? [curpos first_child] : keyboard_firstnode;
        while (p) {
            grab_key([p key], [p state], win, GrabModeAsync);
            p = [p next_sibling];
        }
        if (curpos)
            grab_key(config_keyboard_reset_keycode,
                     config_keyboard_reset_state,
                     win, GrabModeAsync);
    }
}

- (void) grabKeys: (BOOL) grab
{
    [self grab: grab forWindow: [[AZScreen defaultScreen] supportXWindow]];
    AZClientManager *cManager = [AZClientManager defaultManager];
    int i, count = [cManager count];
    for (i = 0; i < count; i++)
    {
      AZClient *data = [cManager clientAtIndex: i];
      [self grab: grab forWindow: [data window]];
    }
}

- (void) interactiveEnd: (AZInteractiveState *) s
                  state: (unsigned int) state
		  cancel: (BOOL) cancel
{
    action_run_interactive([s actions], [s client], state, cancel, YES);
    [interactive_states removeObject: s];

    if ([interactive_states count] == 0) {
        grab_keyboard(NO);
        grab_pointer(NO, OB_CURSOR_NONE);
        [self resetChains];
    }
}

- (void) clientDestroy: (NSNotification *) not
{
    AZClient *client = [not object];
    int i, count = [interactive_states count];

    for (i = 0; i < count; i++) {
        AZInteractiveState *s = [interactive_states objectAtIndex: i];
        if ([s client] == client)
            [s set_client: nil];
    }
}

- (BOOL) chainTimeout: (id) data
{
  [[AZKeyboardHandler defaultHandler] resetChains];
  return NO; /* don't repeat */
}

@end
